mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
69c60912169be2edb60bff74a010f2b15d23e8fe
2294 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69c6091216 |
feat(buzz-agent): back goose's Databricks provider with buzz's PKCE engine
Both sides ran OAuth PKCE against the same Databricks host with separate token caches: goose's providers::oauth for inference, buzz's PkceOAuthTokenSource for the desktop model picker. A user could be authenticated for one and not the other and face two browser flows for one host. Buzz's engine is the hardened one (#5534: O_NOFOLLOW, 0600 from creation, unique temp names) and the only one the desktop can link, so it wins. goose supports this without a fork: DatabricksV2Provider::new takes an oauth token provider closure, and databricks_v2_def::from_env is just the variant passing goose's own. Retries, the agent-session-id header, endpoint resolution and the wire format all stay goose's. Uses bearer_no_browser: inference runs in a headless subprocess where a browser step would hang unseen. Interactive login stays in buzz-agent auth databricks and the desktop picker. A static DATABRICKS_TOKEN still takes precedence, as in goose's own from_env, and no DATABRICKS_HOST falls through to goose's registry unchanged. Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
8e1ba34207 |
refactor(buzz-agent): use goose's skills instead of buzz's duplicate
buzz-agent grew its own skill discovery, prompt index and load_skill tool before goose had one. goose now ships goose::skills plus a SkillsClient registered in PLATFORM_EXTENSIONS, so buzz's version was a strictly smaller duplicate: the same three project directories, but goose also reads the global directories, plugin-installed skills and its own builtins, and supports args templating buzz never had. Delete builtin.rs, builtin_client.rs and hints.rs (1,204 lines) and register goose's skills extension by name so its factory runs. goose declares it unprefixed_tools, so the model now sees load_skill rather than buzz__load_skill. skills.rs keeps only the prompt index, because the goose code that renders it lives on SkillOperation, which is pub(super). The hint-file guard from hints.rs moves there rather than being dropped, and now matches filename literals instead of the bare words so it can document itself. Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
3190102efb |
merge origin/main into micn/buzz-agent-goose-core-v2
Conflicts, and how each was resolved: * crates/buzz-agent/src/mesh.rs + tests/mesh_auto.rs — DELETED. main's #5289 removed buzz-agent's client-side mesh catalog probe on purpose: MeshLLM v0.75 decides per request, the desktop resolves the wire model before spawn (relay_mesh_wire_model), and BUZZ_AGENT_PREFER_MESH_FOR_AUTO is no longer set by anything. Keeping my MeshAutoProvider would have resurrected policy main deliberately retired. * crates/buzz-agent/src/llm.rs — stays deleted; goose owns the transport. main's changes to it were the Databricks OAuth hardening (#5534), which applies to auth.rs, now living in buzz-model-catalog. * crates/buzz-agent/src/config.rs — ours; main's only change was dropping prefer_mesh_for_auto, which this branch had already dropped. * buzz-model-catalog: picked up #5534's O_NOFOLLOW/0600 token-cache hardening, which needs the nix dep the extraction had left behind. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
16b7ae7ce6 |
fix(relay): stop panicking the ingest worker on reactions to project events (#5294)
A NIP-25 reaction whose target is a project root or project comment
(kind
1621 issue, 1618 PR, or a kind-1 comment on one) carries no h tag, so
channel_id is None on the reaction write path. The conformance-trace
emission asserted a channel was always present:
channel: channel_label(channel_id.expect("reaction path has channel")),
so the worker panicked at ingest.rs:2824. The row was inserted before
the
panic, so the client saw a failed request for a persisted event and
retried,
and the duplicate branch carried the same expect, head-of-line blocking
a
durable publish queue forever.
Mirror the message write's three-way split at the same seam:
(Some, true) -> WriteInsert, (Some, false) -> WriteDuplicate, (None, _)
-> WriteInsertGlobal. The conformance vocabulary already models
channel-less
writes; only the reaction path was missing it.
Closes #4936
Signed-off-by: Taksh <takshkothari09@gmail.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
|
||
|
|
d9dc76c0aa |
fix(desktop): bound initial timeline retention (#5603)
## Summary - let Virtua own the initial visible timeline range instead of passing every loaded row to `keepMounted` - populate the existing bounded retention window after the virtualizer reports its first settled viewport - cover a 10,000-row timeline to prevent an all-history initial mount regression ## Why `useTimelineRetention` initialized its retained-key set with every loaded timeline key. Those indices were passed to Virtua's `keepMounted`, effectively defeating virtualization during initial channel positioning until `onScrollEnd` pruned the set. On a large real channel this grew WebContent into multiple gigabytes and blocked the renderer main thread for 20+ seconds while WebKit laid out and painted the retained rows. Starting with no retained rows restores Virtua's visible-range mount; the existing reader-neighborhood and visual-tail retention is populated once the viewport is measured. ## Validation - `node --import ./test-loader.mjs --experimental-strip-types --test src/features/messages/ui/useTimelineRetention.test.mjs` - pre-push hook at `8e86a189de7e9a8f2cb119396c8f912ed9dacd6e`: branch-skew, desktop-check, desktop-typecheck, and all 4,671 desktop tests passed - manual ablation against PR #5599 on the affected profile: catastrophic channel-switch stalls disappeared ## Authorship disclosure Carl implemented and is posting this change on Wes's behalf. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
e8153f8f27 |
fix(relay): log event kind on the HTTP bridge /events line (#5291)
The HTTP bridge request log recorded route, status, and accepted but not the event kind, so typing indicators (kind 7) and their deletions (kind 5) were indistinguishable from real messages (kind 9). Every agent turn produced accepted:true lines whether or not a message was actually sent, which twice led debuggers to conclude a silent agent had published successfully. Add kind to the Ok outcome and the tracing::info line so the publish path is self-describing without a database query. Closes #4676 Signed-off-by: Taksh <takshkothari09@gmail.com> |
||
|
|
397796c5f3 |
feat(tracing): add PostgreSQL tracing spans (#3678)
## Why Expose PostgreSQL datastore latency within existing request traces so slow logical database operations can be identified without recording tenant data or query arguments. ## What - Add client spans around logical PostgreSQL operations across the database facade, search, audit, replica fencing, and command persistence - Use a dedicated `buzz_datastore` target and `db.system.name = "postgresql"` for filtering and backend classification - Exclude health-check database calls and scrub raw identifiers and errors from newly traced paths ## Risk Assessment Medium — this instruments frequently used datastore paths and increases trace volume when enabled, but does not change SQL execution or datastore behavior. Existing OpenTelemetry filtering controls export. ## References - Pre-push clippy and fast unit-test hooks passed Generated with Amp --------- Signed-off-by: David Grochowski <dgrochowski@squareup.com> Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
cf03bd7c37 |
Improve desktop search scoping (#5306)
## What changed - unify Cmd+K and channel Cmd+F around a removable channel or conversation scope - add conservative fuzzy matching for people and channels while preserving exact-match ordering - make scoped message search complete for one-character queries and expose up to 40 scrollable results - keep the pre-scope channel or DM action in the normal results flow so it scrolls away with the list ## Validation - desktop TypeScript typecheck - desktop text-size and file-size guards - focused fuzzy-search unit tests (24 passed) - focused search Playwright coverage (7 passed), including channel and DM copy, one-character results/no-results, 40-result scrolling, and the non-sticky scope action - desktop E2E build - visual review of channel, scoped, expanded-results, and DM states --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> |
||
|
|
cd2aa5c12d |
Add glass appearance and cohesive settings (#5478)
## Summary - add an opt-in native glass sidebar with opacity controls and live theme previews - refine sidebar spacing and Buzz-only active rows while preserving production defaults - unify settings section cards, subtitles, and agent runtime rows ## Validation - repository format, lint, type, and file-size checks - 4,538 desktop tests and 2,270 native desktop tests - desktop and web production builds - 1,261 mobile tests in the completed full gate - focused Playwright appearance, sidebar, settings, pairing, and runtime coverage --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> |
||
|
|
b0795a10ea |
Add Send to channel for thread messages (#5305)
## Summary - Share eligible self-authored or owned-agent thread messages into the parent channel as new top-level messages. - Link the shared message back to the exact root thread with a semantic channel label and excerpt. - Add a dedicated channel-arrow icon plus ownership and navigation coverage. ## Validation - Desktop lint, size, and text guards - Desktop TypeScript build and all 4,543 unit tests - Focused Playwright send-to-channel and thread-link navigation tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
bba3e06386 |
Fix macOS attachment picker lifecycle and allow inert HTML downloads (#5569)
## Problem Canceling the native macOS file chooser leaves the composer's temporary, detached `<input type="file">` without a `change` event or an explicit cleanup path. Opening Finder again immediately creates a second detached input while WebKit may still be unwinding the first picker. The newly selected files can therefore fail to reach the upload pipeline. Drag and drop is unaffected because it bypasses this picker lifecycle. This does **not** add an automatic retry mechanism. “Retry” means the user's next attachment attempt after canceling or after a prior selection. ## Fix - give each composer hook one hidden, body-mounted file input for its lifetime instead of creating a detached one per click - reset and reconfigure that input before every open, replace its handler rather than stacking handlers, and remove it cleanly on unmount - preserve normal selection, cancel then reopen, selecting the same file again, and multi-select behavior - accept canonical `text/html` attachments while continuing to serve and render them strictly as inert downloads - keep XHTML, SVG, JavaScript, and executable MIME types blocked The picker change fixes the ownership/lifecycle bug at its source; it does not retry failed uploads, add delays, or mask errors. ## Testing - mandatory pre-push gate: branch-skew, desktop typecheck/tests/check, Rust tests, and desktop Tauri checks passed on `ea5a97adf957803935b28d63d32f9f332cf65287` - `cargo test -p buzz-media --lib` (110 passed) - `pnpm --dir desktop typecheck` - focused Biome check for the three picker files - picker Playwright regression: cancel/no selection then reopen, select the same file again, and multiple selection (run on the source commit before integration) - HTML live-relay response regression added as ignored E2E because it requires the S3-backed relay harness ## Manual verification Playwright models cancellation with Chromium's `FileChooser.setFiles([])`; it cannot exercise the native macOS Finder panel/WebKit presentation lifecycle. Before merge, manually verify in the built macOS app: 1. select a PNG normally 2. cancel, then immediately reopen and select a PNG 3. select the same PNG on a subsequent attempt 4. multi-select two PNGs --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz> Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
d3ec831e0c |
fix(desktop): preserve fresh channel timelines (#5577)
## Summary - skip the channel subscription catch-up request when the authoritative channel window was fetched successfully within the existing five-minute freshness period - keep the responsive deferred skeleton for populated channel switches instead of briefly rendering empty-channel actions - preserve a real empty-channel intro across the first appended message only after React has committed that empty state This is intentionally narrow. It does not claim to solve the separate sidebar startup cost or general main-thread stalls found during the investigation. ### Related issue N/A — no matching open issue or PR found. ### Testing - pre-push desktop gate on `f1be6beea90b9715e04e5fc65cc5cfbe8210e0d9`: - desktop tests: 4,621 passed - desktop check: passed - desktop typecheck: passed - branch-skew: passed - focused cache/surface/lifecycle tests: 62 passed - manual diagnostic trace after rollback: - 16/16 channel revisits skipped catch-up refresh - 0 revisit refresh starts - 0 populated-channel empty/intro flashes - cached switches retained the deferred skeleton-to-list path No screenshot: the regression is a transient channel-switch state and request behavior, covered by lifecycle tests and the diagnostic trace rather than a stable visual diff. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
4935724494 |
fix(desktop): suppress fresh focus-return refetches for channels and home-feed (#5535)
Fixes the 0.5.9 sluggishness Wes reported in app-slowness-mac (channel list slow, content slow). ## Problem #5490 (shipped in 0.5.9) flipped ~20 query sites to `refetchOnWindowFocus: true` and wired TanStack focusManager to app focus. Instrumented at that exact commit: regaining focus after >60s away fires **7 query fetches within 2ms**, including `get_channels`, which settles at **~3.6s** (production probe: median 3.2s at 1,133 channels — 8 serial round-trips, 1,133-filter last-message batch). In 0.5.8 this burst was zero by configuration. Net: a burst of fetch/parse contention exactly when the user returns to the app. Relay ruled out: v0.2.1 small reads are 2–4ms upstream; nothing in v0.2.0..v0.2.1 degrades the query path. The O(N) `get_channels` design is a pre-existing issue (June analysis) — this PR fixes the new stampede that made it user-visible. ## Fix Raise `staleTime` to 5 minutes on the two expensive focus-refetch families — `channels` and `home-feed` — so a focus return inside that window serves cache instead of refetching. `refetchOnWindowFocus: true` only refetches stale queries, so genuinely old data still refreshes on return. Unchanged: focused polling cadence (60s channels / 30s home-feed; interval refetches ignore staleTime), #5490 blur quiescence (no changes to `useDocumentVisible.ts`/`queryClient.ts`), all push-style invalidation paths (`invalidateQueries` bypasses staleTime), and channels cold-start revalidate (`initialDataUpdatedAt: 0`). ## Validation - New regression test `desktop/src/features/home/focusRefetchPolicy.test.mjs` (4/4): fresh focus return → 0 fetches; stale → 1; polling constants locked. - Pre-push gate at the reviewed tree: desktop-check, desktop-typecheck, full desktop-test **4588/4588**. - Independent adversarial review (Beth): APPROVE at tree `4e2546ec` — verified fresh-skip/stale-refetch against query-core 5.100.14 source, polling-cadence via browser-simulated probe, side-effect sweep of all invalidation paths clean. Sole CHANGE was commit trailers, fixed by amend (tree unchanged). ## Known residual Focus returns after >5min still fire the full burst including the ~3.2–3.6s `get_channels`. This cuts stampede frequency, not magnitude — the O(N) `get_channels` relay path (RESEARCH/GET_CHANNELS_SLOWNESS.md) is the follow-up that fixes magnitude. Diagnosis: Summer (focus profiling) + Morty (relay probe); implemented by Meeseeks; reviewed by Beth; integrated by Rick. Thread: app-slowness-mac e78fad29380d9a0974c9d673910450994a228781ddce133a8cedbd90504d95be. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> |
||
|
|
240cdd3ea1 |
chore: mesh upgrade, clean up legacy special case code, simplify model selection for mesh (#5289)
Shared compute now has exactly two model choices: MeshLLM's virtual `mesh` model, or a model you name. Buzz picks between them in one place, and buzz-agent no longer knows meshes exist. ## What changed - **MeshLLM v0.74.0 → v0.75.1.** v0.75.0 added `degrade_to_single_model`, so a `model=mesh` request is answered by one served model when there is no committee to form, instead of failing. v0.75.1 adds Mesh-LLM#1196, which skips stale pre-0.75 runtime cache entries rather than aborting startup on them — without it, anyone who had run mesh on 0.73/0.74 could not start. - **Deleted the client-side mesh catalog probe.** buzz-agent used to poll `/v1/models` (5s TTL, 30s cooldown, two-observation debounce) to decide whether `mesh` was safe to send. MeshLLM now decides per request, so the polling, its hysteresis, and its 503 fallback are gone. - **One mapping point.** `relay_mesh_wire_model()` turns the stored value into a wire name: `auto` becomes `mesh`, a named model passes through. The spawn env, the ACP harness, and the readiness probe all use it, so they cannot disagree — previously `BUZZ_ACP_MODEL` and the probe both said `auto`, a name the mesh does not advertise. - **Removed the `nostr-relay-pool` advisory exception.** #5404 allowed RUSTSEC-2026-0243 "after mesh-llm migrates to nostr-sdk >= 0.45". v0.75.1 does, so the retired crate is gone from both lockfiles and the exception would only mask a future advisory for it. - **Deleted `scripts/ensure-mesh-native-runtime.sh`** and its six justfile call sites. It built llama.cpp from source into the runtime cache; the app already downloads the signed release runtime itself, and CI never called it. ## Why it is better **−639 lines of Rust.** Availability is decided by the node that knows the answer, per request, instead of by a client cache that could be stale for up to 30 seconds. A second worker joining now takes effect on the next request rather than after two confirming probes. ## Behaviour change A 503 on an explicit `mesh` request takes the ordinary transport retry under the same model instead of failing over to a second one — there is no second model to fail over to now. MoA repairs partial committee results internally before it reaches that point. ## Validation `crates/buzz-relay/examples/mesh_agent_e2e.rs` now sends `mesh` where it previously sent `auto` or the physical model id, so no leg was covering what Buzz actually puts on the wire. 4/4 on gemma-4-E4B, gemma-4-26B-A4B, and Qwen3-8B — including a real ACP tool call through `mesh` into buzz-dev-mcp, asserted by reading the written file back off disk. Hand-tested in the desktop app on both gemma-4 sizes: picked Auto, agent logged `model_id=mesh`, replied in channel. ## Not covered A committee that forms and then loses a worker returns 502, and that needs two workers to reproduce — not testable on one machine. --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
83ca595ada |
fix(desktop): preserve theme when opening communities (#5266)
**Category:** fix **User Impact:** Previously selected community themes are preserved when opening a relay through onboarding, while first-time theme migration still completes for communities with no saved theme. **Problem:** During community initialization, desktop queried theme history before establishing live delivery. If a replacement event arrived while an empty history query was in flight, the client could incorrectly treat the theme as absent and publish the default over the user's saved selection. **Solution:** Subscribe before fetching history, expose whether live readiness reached EOSE, flush buffered live events before resolving EOSE, and retain the newest delivered replacement through hydration. Seed the inherited/default theme only when both live and history snapshots reach EOSE with no valid or unreadable event; subscription failures, CLOSED, readiness timeout, relay failure, and unreadable events fail closed without publishing. <details> <summary>File changes</summary> **desktop/src/shared/api/relayClientSession.ts / relayClientShared.ts / relayClosedRecovery.ts** Distinguish EOSE from CLOSED/timeout readiness and flush buffered events before resolving an EOSE fence. **desktop/src/shared/theme/CommunityThemeController.tsx** Seed and complete first-community migration only for confirmed absence; uncertain hydration remains non-publishing. **desktop/src/shared/theme/communityThemePreference.ts** Keep the inherited appearance for the first migrated community and the stable default for later empty communities. **desktop/src/shared/theme/communityThemeSync.ts** Arbitrate live and history results into valid, confirmed-absent, invalid, or unavailable hydration outcomes. **Tests** Cover EOSE/CLOSED readiness, subscription failure, timeout, unreadable and live-racing events, no-op initialization, and first-to-later community fallback isolation. </details> ## Reproduction steps 1. Save a non-default appearance for a community relay. 2. Remove the community locally, then open the same relay again through onboarding. 3. Arrange for the saved replacement event to arrive live while the initial history query returns empty. 4. Confirm the saved appearance remains selected and the client does not publish the default theme over it. 5. On an account with no theme records, open a first empty community and confirm its inherited appearance is migrated; open a later empty community and confirm it starts from the stable default. ## Validation - Pre-push desktop checks, typecheck, and full desktop tests: passed at `f79556b0e` - Focused theme/relay readiness tests: 38 passed - Desktop file-size ratchet and diff check: passed --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>mobile-v0.10.0-rc.1 |
||
|
|
7eb8cc5a5f |
fix(link-preview): resolve YouTube videos through oEmbed (#5520)
**Category:** fix **User Impact:** YouTube video links now resolve into reliable previews instead of intermittently appearing as bare links. **Problem:** Buzz intentionally reads at most **256 KiB** of page HTML when building a generic link preview. The YouTube response that exposed this bug was roughly **1.3 MiB**, with its Open Graph metadata beginning around **686 KiB**—well beyond Buzz's bounded read—so extraction returned no usable preview. YouTube can move that metadata between responses, which explains why the same link may appear to work in one build or request and fail in another; raising the generic cap would increase bandwidth and allocation for every site while still scraping an unstable application document. **Solution:** Route recognized YouTube video URLs through YouTube's structured oEmbed endpoint instead of parsing raw watch-page HTML. The provider response is capped at **64 KiB** and retains Buzz's existing HTTPS validation, pinned DNS/SSRF protection, disabled redirects, timeouts, metadata bounds, and thumbnail sanitization. Provider failures return no preview rather than falling back to fragile HTML scraping, and embed URLs are canonicalized safely, including percent-encoded video IDs. <details> <summary>File changes</summary> **desktop/src-tauri/src/commands/link_preview.rs** Recognizes supported YouTube URL forms, fetches bounded JSON metadata from YouTube oEmbed, canonicalizes embed links, and adds response, URL-boundary, malformed-data, resource-limit, and encoded-ID regressions. **desktop/src-tauri/Cargo.toml** Declares percent decoding as a direct desktop dependency for safe embed-ID canonicalization. **desktop/src-tauri/Cargo.lock** Records the direct dependency in the desktop package lock entry. </details> ## Reproduction Steps 1. On the base branch, paste a YouTube URL whose Open Graph metadata falls beyond the first 256 KiB of the raw watch-page response and observe that no preview is produced. 2. Run this branch and paste a YouTube watch, mobile, music, `youtu.be`, Shorts, live, or embed URL into the composer. 3. Confirm the preview resolves with the video's title, creator, and sanitized thumbnail without downloading the full watch-page HTML. 4. Try an embed URL with a percent-encoded ID, such as `https://www.youtube.com/embed/%64Qw4w9WgXcQ`, and confirm it resolves to the same video. 5. Try a YouTube lookalike domain or an embed ID containing encoded separators and confirm it is not routed through the provider path. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
5e4d0fe925 |
fix(buzz-agent): harden Databricks OAuth token cache and callback (#5534)
Hardens the Databricks PKCE OAuth code in `crates/buzz-agent/src/auth.rs`. Two fixes. ## Token cache is owner-only across its whole lifecycle, and race-safe The PKCE cache holds both the access and refresh tokens, but `save()` wrote it with a bare `fs::write` + `fs::rename`. Under a `022` umask the file landed world-readable, and the fixed `*.json.tmp` temp name races across concurrent savers sharing `$HOME` — one writer's `rename` can fail on another's half-written temp. **On write**, `write_private_cache()` creates a temp file with owner-only permissions from the moment it exists — mode `0o600` on Unix via `OpenOptions::mode` — writes and fsyncs it, then renames over the destination. The rename swaps the inode wholesale, so a pre-existing cache file with loose permissions is *replaced* by the new private inode rather than inheriting its mode. `unique_suffix()` (getrandom, timestamp fallback) gives each write a distinct temp name, and a drop guard removes the temp on any failure path. **On load**, owner-only is enforced as a cache lifecycle invariant, not just a write-path property. A world-readable cache left by an older buzz-agent was previously read straight into memory and returned on the fresh cache-hit path without ever invoking `save()`, so a token file with no advertised expiry could stay exposed indefinitely. `read_cache()` now funnels every load — initial and cross-process re-reads — through `read_private_cache()`, which on Unix opens with `O_NOFOLLOW` (kernel-level symlink refusal, no stat/open TOCTOU), requires a regular file, and `fchmod`s the pinned handle to `0o600` when any group/other bit is set. A cache that cannot be secured is treated as absent, so callers fail closed to a fresh flow rather than trusting an exposed file. ## OAuth callback no longer reflects untrusted input The localhost callback embedded the untrusted `error` query param straight into the HTML response — an XSS sink on the redirect page — and routed that same raw value into the error string that reaches the logs. `callback_outcome()` is now a pure function returning `(result, static_page)`: the browser always sees a fixed literal page that embeds no request parameter, and failure detail travels only through the result channel. `sanitize_callback_detail()` strips control characters (CR/LF log-line injection) and caps length before that detail enters the error string bound for the logs. ## Deferred: Windows owner-only ACLs Windows owner-only protection is out of scope for this change. The goose-parity route (`CreateFileW` with an owner-only SDDL `D:P(A;;FA;;;OW)`) requires `unsafe` FFI, which this crate's `#![forbid(unsafe_code)]` prohibits; reconciling that conflict is a separate decision. Both platform seams — `create_private_temp_file` (write) and `read_private_cache` (load) — have a `#[cfg(not(unix))]` branch that relies on the default per-user ACLs and is the drop-in point if Windows protection is added later. No new dependency and no `unsafe` are introduced here. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> |
||
|
|
6c7648f720 |
fix(deps): patch eight nostr advisories instead of ignoring them
I reported the Security CI failure as not fixable here and something for main to add to deny.toml's ignore list. That was wrong: the advisories carry a semver-compatible fix, and cargo-deny prints it. cargo update -p nostr 0.44.6 -> 0.44.8 cargo update -p nostr-relay-pool 0.44.1 -> 0.44.3 That clears all eight: RUSTSEC-2026-0224/0225/0226/0231/0232 and the three other nostr parsing advisories. cargo-deny check now reports advisories ok, bans ok, licenses ok, sources ok. Lockfile-only -- no manifest change, no new ignores, and nothing added to deny.toml. RUSTSEC-2026-0243 (nostr-relay-pool unmaintained) stays ignored; that one genuinely needs the nostr-sdk 0.45 migration. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
45584a9c7a |
refactor(buzz-agent): hold the provider on buzz's side, not in goose's session row
I claimed the remaining session row was needed because update_provider persists the model config there and goose reads it back for the context limit. That was wrong, and the fix is straightforward: buzz builds the provider and model config itself, so it can hold them. New crate::model::SessionModel owns the pair. The turn reads the provider and config from it instead of Agent::provider and model_config_for_session, and session/set_model swaps it there rather than calling update_provider. That removes the last write buzz makes to goose's store. The row itself still has to exist, but for a different reason than I gave. Agent::add_extension_inner (goose agent.rs:1465) resolves each MCP extension's working directory by loading the session, and fails setup if the row is missing -- 'Failed to get session: Session not found'. I found that by removing create_session and watching every tool-using test fail. So: one row per session, created for extension setup, and zero writes after it. Removing it needs goose to take the working directory as an argument instead of reading it back. Verified: a session with an MCP server, a tool call and two turns leaves 1 sessions row and 0 messages rows, still recalls turn 1 in turn 2, and still advertises 9 catalog models. handtest.py --all 18/18; 87 unit + 21 integration green. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
ce844719f8 |
refactor(buzz-agent): keep the turn conversation in memory, not sqlite
The turn loop round-tripped every message through goose's SessionManager
-- a sqlite database -- once per round. That was never required. None of
the goose APIs buzz calls reads the store:
- Provider::stream takes &[Message].
- Agent::dispatch_tool_call takes &Session but reads only working_dir
and id.
- context_mgmt::{check_if_compaction_needed, compact_messages} read
usage.total_tokens and model_config, and carry session_id only into a
task-local that becomes an agent-session-id HTTP header for provider
request attribution (goose/src/session_context.rs).
So the turn now owns a Session value directly (crate::turn_state) and
buzz-agent keeps the conversation across turns itself. goose still gets
the &Session its signatures ask for; nothing backs it with a database.
This is the right shape for buzz regardless of dependencies: a Buzz
agent's durable record is the relay, not a local sqlite file. Agents are
chatty and long-lived, and every one of them was growing a database
nothing ever read back.
One row remains, created at session/new: update_provider persists the
provider/model config against the session id, and that config is what
goose reads back to resolve the context limit for compaction. Declining
it would make auto-compaction fire at the wrong point. Zero message rows
are written for a conversation of any length.
Verified: a two-turn conversation recalls the first turn's content with
0 rows in the messages table; handtest.py --all 18/18; 84 unit + 21
integration green.
Also fixes the Desktop Core CI failure this branch introduced: the log
filter added earlier pushed runtime.rs past the 1000-line ratchet, so it
moves to its own module with its tests.
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
|
||
|
|
979eff99fd |
refactor(buzz-agent): express the round budget as a goose Operation
First step of moving buzz's loop decisions into goose's StateMachine, one
at a time (PLANS/BUZZ_OPERATIONS_MIGRATION.md). The remaining decisions
still live in loop_drive.rs; this adds the gate they will move into.
Two things the state-machine shape buys immediately:
- The budget now counts assistant turns since the kickoff message rather
than loop iterations, so it survives a process restart. The loop-local
counter did not.
- Precedence between the guards becomes list order rather than control
flow, which is what makes the remaining conversions safe to do
incrementally.
Deliberately not goose's MaxTurnsOperation: that appends an assistant
message ('Would you like me to continue?') before yielding. buzz-acp
publishes assistant messages into a channel, so a synthesised one would
be indistinguishable from the agent saying it. buzz signals the exhausted
budget through the max_turn_requests stop reason instead.
Two things found while building it, both now pinned by tests:
- goose's operation helpers (messages_since_kickoff, assistant_turn_count)
are pub in a *private* module, so they are not reachable from outside
the crate -- only the names re-exported from state_machine are.
Reimplemented here against goose's semantics.
- A mid-turn steer is user-visible, so it becomes the new kickoff and
restarts the budget. That is the behaviour we want, but it is emergent
rather than chosen, so a test pins it.
StepResult cannot carry a stop reason, so operations record one in a
shared Outcome cell; first writer wins, since the machine stops at the
first operation that applies.
Verified: max_rounds=2 against a real provider returns max_turn_requests
with the gate firing from buzz_agent::ops; handtest.py --all 18/18;
79 unit + 21 integration green.
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
|
||
|
|
c312a70e53 |
fix(buzz-agent): mark steered messages with with_steer()
goose's own steer path sets metadata.steer (agent.rs:540) and its ACP surface republishes it as _meta.goose.steer. buzz-agent queued steers as plain user messages, so nothing downstream could tell a mid-turn steer apart from the user simply sending another message -- which is the one distinction the flag exists to preserve. buzz-acp does not read it today; this keeps the conversation record honest for anything that later does. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
c918877d60 |
fix(buzz-agent): stop rendering every AGENTS.md into the prompt twice
buzz-agent scanned for hint files itself and added the result as a system-prompt extra. But goose's PromptManager::build_system_prompt already calls .with_hints() (prompt_manager.rs:255), which runs the same walk. Every AGENTS.md therefore reached the model twice -- once under our '## Project Hints', again under goose's '### Project Hints'. Verified against a scratch repo with a canary string: 2 occurrences before, 1 after. The walk really is the same algorithm, which is why this went unnoticed: goose's find_git_root + get_local_directories collect cwd->root and reverse for root->cwd ordering, exactly as buzz's did. goose's loader is a superset -- .goosehints as a second filename, CONTEXT_FILE_NAMES to configure the list, gitignore-aware filtering, and @file expansion bounded at the git root. So hint-file loading is deleted here and left to goose. What stays is skill discovery, because load_skill is served by our builtin_client and needs the same SkillEntry list the tool dispatches against. hints.rs 732 -> 479 lines. A guard test fails if hint-file loading is reintroduced, since the duplicate rendered silently -- the prompt was merely longer, never wrong-looking. Verified end-to-end against Anthropic: handtest.py --all, 18/18, persona and hints still reach the model and load_skill still resolves. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
626b079fa3 |
fix(buzz-agent): keep agent conversations out of the user's goose session DB
goose's SessionManager::instance() resolves to Paths::data_dir(), so buzz-agent was persisting every turn into the same sessions.db as the user's own goose CLI history. On this machine that file is already 132 MB and 892 sessions, with desktop agent turns from minutes ago interleaved into it. Not a correctness bug -- SQLite handles concurrent writers -- but agent conversations do not belong in a human's personal session history, and buzz-agent should not grow a shared file it does not own and cannot prune. Resolves its own directory instead: BUZZ_AGENT_SESSION_DIR when absolute, else <data-dir>/buzz-agent, else goose's singleton. A relative env value is ignored rather than resolved against the agent's CWD, which is the user's workspace. Deliberately not GOOSE_PATH_ROOT: that would move goose's config and keyring lookups too, and provider configuration should keep resolving where goose puts it. Resolved once behind a OnceLock -- SessionManager::new builds its own SQLite pool, so a per-session call would open a pool per session against the same file. Verified: with BUZZ_AGENT_SESSION_DIR set, the turn's session lands in that directory and the shared DB is untouched. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
20a731b6e8 |
fix(desktop): admit buzz_agent logs to per-agent log files
buzz-acp inherits its stderr to the agent subprocess (acp.rs:463), so the desktop's RUST_LOG filter for the harness also decides what the agent can write to disk. It named only buzz_acp, so every buzz-agent diagnostic was filtered out before reaching the per-agent log. That is invisible until something goes wrong: an agent misbehaving in the desktop left no trace of compaction, the _Stop veto, max-rounds, or provider errors -- exactly the lines you need to tell whether a turn did what it should. Found while trying to confirm from the logs alone that a desktop agent was running the new loop; the logs could not answer it. Adds buzz_agent=info to the default, appends only the missing directive when an operator filter is present (so RUST_LOG=buzz_agent=debug is not downgraded to info), and takes a fully-specified filter verbatim. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
1d098d6d52 |
test(buzz-agent): hand-test script driving a real provider over ACP stdio
Closes the biggest coverage gap on this branch: every automated test talks to a fake SSE server, so nothing proved the loop works against a real model. scripts/handtest.py starts the actual binary, speaks real ACP to it, and asserts on the five behaviours that are easy to break and hard to notice: basic persona + AGENTS.md hints reach the model; catalog populated tools a real MCP tool is dispatched and its output comes back stop-veto _Stop blocks end-of-turn, and the cap still releases it cancel cancel mid-tool leaves no tool call spinning steer a mid-turn steer is absorbed without restarting the turn All 18 checks pass against Anthropic claude-sonnet-4-6. Each mode gets a fresh process so one mode's conversation cannot pollute the next. HANDTEST.md updated: describes the script, and its 'never run against a real provider' known gap is replaced with what is now actually covered. Also corrects the opening paragraph, which still said goose owns the loop. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
9869f24319 |
refactor(buzz-agent): buzz owns the agent loop; goose supplies the parts
Inverts the previous design. buzz-agent no longer calls
goose::agents::Agent::reply -- it drives its own round loop and calls
goose for the four heavy components:
model call Provider::stream (via Agent::provider)
tool surface Agent::list_tools
tool execution Agent::dispatch_tool_call
system prompt PromptManager (our own instance)
compaction context_mgmt::{check_if_compaction_needed, compact_messages}
Under Agent::reply every buzz-specific behaviour had to be smuggled in
around goose's turn policy: the _Stop veto needed an outer loop purely to
re-enter reply(), and [Reflect] had to be delivered as a *steer* because
the tool result itself was out of reach. Owning the loop removes the
smuggling -- the veto is a branch, and [Reflect] goes back on the tool
result where buzz-agent originally put it and where the model reads it in
context.
New modules:
loop_drive.rs the round loop: inference, tools, compaction, _Stop veto,
steer drain, cancellation, max-rounds bound
tools.rs parallel dispatch, announce->terminal wire invariant,
[Reflect] on failure
prompt.rs our PromptManager (goose's is pub(super) to Agent::reply)
steer.rs our steer queue (goose's drain is pub(crate) to reply)
agent.rs shrinks to what goose knows nothing about: ACP session/update
emission and the keepalive ticker. Tool lifecycle now emits from tools.rs,
which is the only place that knows when a call starts and ends -- emitting
from streamed content as well would double-announce every tool.
Behaviour preserved and covered by the existing suite: Fizz persona,
_Stop veto (cap 3), [Reflect] (cap 8), _PostCompact re-injection,
steering without cancellation, cancel leaving no unresolved tool call,
usage accounting, and the model catalog.
81 unit + 21 integration tests pass; clippy clean (the one hints.rs
warning predates this branch).
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
|
||
|
|
80a9f9a360 |
chore(buzz-agent): bump goose pin to bf332b9 (unrolled loop) and fix rmcp 3.x breaks
Moves the goose git pin from 305849b to bf332b9, which is past ca52cce (#9574, the unrolled agent loop). Consequences of the bump: - goose now uses rmcp 3.x; buzz-agent's own rmcp dep moves 1 -> 3 so the two do not resolve to different crate versions of the same types. - rmcp 3 renames Content -> ContentBlock and adds fields to ListToolsResult; builtin_client.rs updated to match. - buzz-model-catalog needs an explicit dirs dep after the merge. - TurnTotalState / PricingIdentity, added on main while this branch was away, ported back into types.rs for wire.rs's usage_update_payload. cargo check -p buzz-agent passes. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
d35f7042a5 | Merge origin/main into buzz-agent-goose-core | ||
|
|
be48ce98bd |
fix(link-preview): reliably render previews sent right after they resolve (#5245)
## Overview **Category:** fix **User impact:** Link previews no longer disappear when a message is sent while preview metadata or media is still settling. Fast Enter, rapid Enter, and confirmed-draft auto-send now preserve the preview without duplicate sends or stale tags. **Problem:** The composer could look ready before its sender-authored snapshot tag existed. Send paths could then race preview resolution/upload, while debounced preview state could attach a tag for a URL that had already been removed. The same timing also caused confirmed-draft auto-send to be consumed without sending. **Solution:** - Debounce preview resolution to avoid card flicker while typing, then disable every submit path while a supported external preview settles. A 2-second escape cap still permits a bare-link send if resolution stalls. - Keep submit synchronous: acquire a composer-local lock before asynchronous send work, read ready tags from the live URL set, and reject Enter/form submits while a snapshot is pending. - Retry confirmed-draft auto-submit until preview settling clears, then submit exactly once. - Upload thumbnail and favicon independently. A failed upload shows a toast and degrades to the surviving media (or text-only) rather than leaving the card spinning. - Exclude message-edit mode from preview resolution, upload, and Save gating. Edit-time preview snapshots remain follow-up #5273. - Canonicalize fragment-bearing URLs for preview lookup/snapshot identity while preserving the original fragment links in message text. ## Link preview state walkthrough Captured using PR #5245's actual public Open Graph metadata and artwork. The deterministic E2E bridge controls only upload timing so the transient disabled state can be captured reliably. | State | Expected behavior | Screenshot | | --- | --- | --- | | **1. Snapshot upload pending** | The real PR preview is visible, but Submit remains disabled until its sendable snapshot tag is ready. Click and Enter cannot send a bare link during the settling window. |  | | **2. Snapshot ready** | Once snapshot upload settles and the tag is ready, the same preview remains and Submit becomes active. |  | | **3. Message sent** | The sent event carries the snapshot tag and renders the PR title, description, and artwork inline instead of degrading to a bare URL. |  | ## Regression coverage - Enter during metadata resolution or snapshot upload cannot send early. - Paste-and-immediate-Enter sends after settling; rapid Enter submits exactly once. - Confirmed-draft auto-send waits for settling and fires exactly once. - Removed/replaced URLs cannot leak stale snapshot tags or media refs. - Thumbnail upload failure toasts and sends with the surviving favicon. - Edit mode does not resolve/upload previews or gate Save. - Fragment variants share a canonical preview while original fragment links remain clickable. - Existing ready-preview, suppression, bare-link fallback, and multi-preview behavior remains covered. ## Reproduction steps 1. Open a channel and paste a supported external URL into the composer. 2. Press Enter immediately, before preview metadata/media finishes settling. 3. Before this fix, the event could be sent without its preview snapshot (or confirmed-draft auto-send could be lost). With this fix, submit waits behind the disabled state and fires once with the matching snapshot tag. 4. Remove or replace the URL and press Enter inside the debounce window. The sent event contains tags only for URLs still present in the submitted content. ## Validation All required PR checks are green, including Desktop Core, Desktop Smoke E2E shards, Desktop E2E Integration shards, macOS build, security checks, and DCO. --------- Signed-off-by: Taylor Ho <taylorkmho@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> |
||
|
|
7e6e9c547f |
fix(link-preview): restore Buzz entity link cards (#5494)
**Category:** fix **User Impact:** Buzz pull request, issue, and repository links now show compact, useful metadata cards in received messages, including messages sent by agents and the CLI. **Problem:** Sender-authored snapshots protect recipients from external preview fetches, but that change also removed recipient-side cards for trusted Buzz entity links when the sender did not attach snapshots. **Solution:** Resolve recognized Buzz entities only against the active relay and show signed repository identity, title, and compact builder context with the current inline Buzz mark in the favicon slot, but without avatars, thumbnails, or external image fetches. Entity metadata wins over conflicting sender snapshots, while unsupported or unavailable metadata retains a safe text fallback. <details> <summary>File changes</summary> **desktop/playwright.config.ts** Adds the entity-link regression spec to the smoke test project. **desktop/src/features/messages/ui/useComposerLinkPreviews.tsx** Treats recognized Buzz entity cards as complete without generating snapshot tags and retains fallback cards when relay metadata is absent. **desktop/src/shared/lib/useResolvedLinkPreviews.test.mjs** Covers kind-scoped entity detection, trusted relay metadata, root-scoped lifecycle queries, exact single-repository root binding, image-less pending state, and fallback behavior. **desktop/src/shared/lib/useResolvedLinkPreviews.ts** Resolves signed repository, pull request, and issue metadata from the active relay. Entity roots fail closed unless they carry exactly one matching repository tag; lifecycle queries are root-scoped before limits; successful metadata remains stable until relay/community reset, and PR commit context uses the immutable root event rather than an unindexed update query. **desktop/src/shared/ui/compact-link-preview-attachment.tsx** Uses Buzz repository identity as the compact card provider and avoids reserving thumbnail space for image-less entity cards. **desktop/src/shared/ui/markdown.tsx** Routes message cards through the combined entity/snapshot preview hook. **desktop/src/shared/ui/markdown/useMessageLinkPreviews.test.mjs** Proves relay-authenticated entity metadata beats a forged sender snapshot while preserving mixed-link content order. **desktop/src/shared/ui/markdown/useMessageLinkPreviews.ts** Combines recipient-resolved Buzz entities with sender-authored external snapshots using explicit trust precedence and first-seen ordering. **desktop/tests/e2e/entity-link-recipient-cards.spec.ts** Exercises repository identity, PR workflow context, repository metadata, image-less rendering, and composer send behavior for agent/CLI-style entity links. </details> ## Reproduction steps 1. Open a channel containing a message sent without `link-preview` tags whose content includes valid `buzz://pr`, `buzz://issue`, or `buzz://repo` links. 2. Confirm each card shows its repository identity and signed title; PRs/issues also show compact lifecycle context, and repositories show description/status/default branch. 3. Confirm the cards use the Buzz mark in the favicon slot with no avatar, thumbnail, or reserved image area. 4. Compose and send a message containing a Buzz entity link; confirm sending is not blocked waiting for a snapshot. 5. Send a message containing both a Buzz entity link and a snapshot-backed HTTPS link; confirm cards follow content order and the HTTPS link remains sender-snapshot-only. ## Screenshots ### Recipient view — Buzz-branded metadata cards Repository identity, title, and compact builder context render with the current inline Buzz mark in the favicon slot and no avatar, thumbnail, or reserved image space.  ## Validation At commit `7bc70b0a9f70392bd062ed25b1d2362cc4021a40` with a clean working tree: - Pre-push hooks passed: branch skew, desktop check, desktop typecheck, and full desktop unit suite - Full desktop unit suite: 4,560 passed - Purpose-built Playwright regression after a fresh E2E build: 2 passed - Screenshot regenerated from the same commit and visually inspected Originating conversation: Buzz channel `c2859932-b679-4091-9c7e-f5a65deddd64`, thread `93c3e7be59a8d1ec10b4992efd783a2a79f253a10f10d39746c6ad41b0d5bb42`. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
538e5e113f |
chore(release): release Buzz Desktop version 0.5.9 (#5521)
## Buzz Desktop release v0.5.9 - **Frozen main:** `f8f2ef0440e7a074223ec04dc3b32d817b8b9d9b` - **Reviewed candidate:** `ee33722615ca1e7b8efb03e2ed641d99448c8899` - **Previous desktop release:** `desktop-v0.5.8` - **Proposed immutable tag:** `desktop-v0.5.9` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
f8f2ef0440 |
feat(cli): add --visibility flag to channels update (#5119)
## Why `buzz channels update` could already change name, description, and TTL, but the SDK/relay/DB path for channel visibility was unreachable from the CLI. ## What - Add `--visibility open|private` to `buzz channels update` - Pass the visibility value through to `build_update_channel` - Add guard tests proving empty updates still fail and visibility-only updates are accepted ## Risk Assessment Low — this is limited to the buzz-cli update command and uses existing SDK validation plus existing relay/DB handling. ## References - Spike notes: `RESEARCH/SPIKE_CHANNEL_VISIBILITY_TOGGLE.md` - Local validation: `cargo test -p buzz-cli` Generated with Codex Signed-off-by: Cameron Hotchkies <chotchkies@block.xyz> Co-authored-by: Lazy Joe <dbd8c9941ba6dafebcef0abc015b65e75d52e7452f2ce483c9c3fd4d180f2504@buzz.block.builderlab.xyz> |
||
|
|
3f2f32641f |
Polish desktop onboarding flow (#5310)
## Summary - standardize onboarding navigation and horizontal step transitions - refine the avatar editor with live preview, segmented modes, search, skin tones, and reduced-motion-safe feedback - simplify harness/default-model actions and supporting copy ## Testing - desktop typecheck and static guards - desktop E2E build - 9 focused onboarding smoke tests - 4 focused onboarding/profile integration walkthroughs - 4,535 desktop unit tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> |
||
|
|
07a3c768d6 |
fix(desktop): quiesce renderer polling while hidden (#3677) (#5490)
Fixes #3677. ## Problem The renderer never quiesces: recurring timers, query polling, and re-render tickers run at full rate whether the window is visible, hidden, or minimized. Measured on a live installed app: **27.5% mean renderer CPU visible vs 28.7% hidden** (60×1s `ps` samples of the WebContent process; `sample(1)` dominated by `WebCore::timerFired`/ThreadTimers, microtask checkpoints, JSON parsing, style matching). Matches all three reproductions in #3677 (macOS prerelease, Linux/WebKitGTK A/B/A minimize test, stable macOS). Per-timer instrumentation (dev build, wrapped `setInterval`/`setTimeout`/rAF) attributed the recurring work: `useNow` 60 fires/min, 40 active TanStack refetch intervals, agent-turn pruning 12/min, auto-restart ticks, huddle/reminder polls — none visibility-gated. ## Fix (two-tier gating, standard mechanisms only) Two separate signals in `desktop/src/shared/lib/useDocumentVisible.ts`, because they mean different things and (see residuals) are delivered differently on macOS: - **`useDocumentVisible`** — true Page Visibility only (`document.visibilityState`). Gates local UI work that must keep running on a visible-but-unfocused window: `useNow` relative clocks, agent-turn pruning, huddle bar state/model-status polling, auto-restart tick. Hidden ⇒ paused; `useNow` snaps to fresh `Date.now()` on return. - **`useAppFocused`** — visible AND `document.hasFocus()`. Gates network refetch polling only (`useFocusedRefetchInterval`, ~15 query families: forum/home/agents/channels/templates/emoji/user-status/projects/workflows/persona-catalog/pulse/presence-list). TanStack's `focusManager` is wired to this signal (idempotent, single install) with `refetchOnWindowFocus: true`, so stale queries refresh promptly on return. Deliberate side effect, documented in code: query retries pause on blur; mutations and the presence heartbeat (`retry: 0`) are unaffected. - **Never gated:** reminder due-notification poll (fires while hidden/unfocused — extracted to `reminderNotificationPoll.ts` with regression test), huddle pipeline hot-start (`check_pipeline_hotstart` survives backgrounding for the duration of a huddle), relay stall watchdog, presence heartbeat. Live WebSocket delivery untouched throughout. - Huddle model-status indicator now clears only on huddle phase end, not on visibility/focus changes. ## Validation - Instrumented dev build, populated channel, fires/min: **visible+focused** unchanged (`useNow 60 / prune 12 / watchdog 6 / query 4 / auto-restart 4 / low-rate huddle/reminder/presence`); **visible+blurred**: query polls 0, UI clocks continue (`useNow 60 / prune 12`), reminders 2, presence live; **truly hidden**: only watchdog 6, reminders 2, presence ~2 — everything else 0. Return restored visible+focused, selection preserved, queries refreshed. - Hide-vs-blur decomposition (instrumented probe instance, AppleScript-driven): on macOS WKWebView, Cmd-H / minimize / full occlusion did **not** reliably produce `visibilityState === "hidden"` — they reliably produced focus loss. The CPU-dominant quiescence path on macOS is therefore the focus gate; the visibility gate is exercised fully on platforms that report hidden (e.g. WebKitGTK minimize per the Linux repro). - Gate-regression tests: signal separation, `useNow` hidden-pause + fresh-snap on return, focus-gated interval pause/resume-with-refresh, reminder delivery while hidden+unfocused (5 new, plus primitive wiring tests). - Push gate: desktop check, typecheck, full desktop suite **4549/4549** at `1237548d1`. ## Known residuals - **macOS hidden-signal limitation:** because WKWebView rarely reports `hidden` on app-hide/minimize, hidden-only consumers (`useNow`, prune, huddle UI polls) may keep ticking on macOS when the app is hidden. These are cheap local timers; the expensive network polling still quiesces via focus loss, which is what the measured 28% CPU was attributed to. If the residual local-timer cost proves measurable, the follow-up is bridging Tauri window hidden/minimized events into the visibility signal. - End-to-end CPU confirmation on a packaged build is the post-merge follow-up (against the 28% idle baseline). - Visible-state costs (skeleton animation pileups on stuck loading views, per-poll JSON payload churn) are intentionally out of scope — separate follow-up issue. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> |
||
|
|
2777189d96 |
fix(channels): restore member invitations to private channels (#5493)
## Summary - restore private-channel invitations for every active member - keep owner/admin-only enforcement for elevated role grants, active role changes, and removals - preserve #4612's unrelated Desktop/mobile failure handling and hardening - add relay coverage for the ordinary actor/target role matrix (`member`, `guest`, `bot`) ## Validation - pre-push hook passed on `7de700e17642ad7e10155f9537033168d9249268`: branch skew, Desktop checks/typecheck/tests/Tauri checks, mobile tests, and Rust tests - `cargo test -p buzz-test-client --test e2e_relay --no-run` - `cargo fmt --all -- --check` - `git diff --check` - Donut and Mongo independently reviewed the cross-layer authorization behavior; Donut's role-matrix coverage finding is addressed in this revision --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
5a3b3d2322 |
perf(ci): experiment with sccache for relay builds (#5224)
## Summary - add a SHA-pinned sccache action to reuse unchanged Rust compilation units when the exact relay artifact cache misses - keep pull requests read-only while preserving cache writes for trusted `main` and `release` pushes - stop saving isolated exact relay-artifact caches from PRs, reducing cache churn - preserve the exact artifact cache as the zero-build fast path ## Why this is an experiment The relay artifact job currently misses its exact cache whenever any file under `crates/**` changes, forcing a full workspace rebuild. PR #4975 spent roughly 21 minutes in that job for a one-file `buzz-sdk` change. sccache targets the relevant reuse boundary—individual compiler inputs—but the repository cache pool is already under heavy eviction pressure, so this PR does **not** claim a proven timing win yet. ## Safety - `Mozilla-Actions/sccache-action` is pinned to commit `fc920bf0ec8de6ee65d409111f7ec508035751ba` - `RUSTC_WRAPPER` is scoped only to `Build relay artifacts` - PRs use `READ_ONLY`; trusted `push` runs (`main` and `release`) use `READ_WRITE` - the existing exact finished-artifact cache remains the first/fast path - finished artifacts are saved only by trusted pushes, preserving the former trust boundary - workflow permissions remain `contents: read`; no `pull_request_target` path is introduced - the pinned action automatically emits sccache hit/miss/error/write/duration statistics in its post-job hook ## Validation - `actionlint .github/workflows/ci.yml` - `git diff --check` - desktop release-cache contract test - release-ref contract test - independent code-shape reviews from Princess Donut and Mongo: 9/10, no remaining findings ## Measurement plan 1. purge obsolete PR-scoped `relay-artifacts-*` cache entries before measurement 2. merge/push a trusted writer to populate sccache 3. run a representative one-crate PR 4. compare relay job duration and automatic sccache statistics against the 21–22 minute baseline 5. retain this only if the warm run demonstrates material improvement --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
9c074bb89b |
fix(desktop): bound nine unbounded localStorage stores (#5454)
Part of #5418 (Phase 1, lane A). Companion to #5453 (TTL sweep). ## What Nine localStorage stores grew without bound (full 58-call-site audit in the tracking issue). Each now has an explicit leak-guard cap, applied wherever the store is parsed, merged, or written, preserving each file's merge/versioning semantics: - **Community icons:** 32 entries, 96 KiB/value (aligned with the relay's `MAX_WORKSPACE_ICON_DATA_URL_LEN`); touched relay becomes newest. - **Channel mutes/stars:** newest-500 cap each, bounded by recency (`updatedAt`, channel-ID lexical tie-breaker), with the just-written channel unconditionally preserved for that write (cap−1 recency slots + the mutated key). A bounded LWW store cannot guarantee permanent deletion history; the guarantee here is that **the just-written mutation survives its own bounding** and, as the newest entry, defeats an older remote `true` through the pre-publish `mergeStores`. Known residual (accepted): `updatedAt` is whole-second, so two distinct mutations inside the same second at exact capacity can still evict the earlier one before the debounced publish — same root cause as the merge-path same-second tie, tracked for the follow-up precision fix rather than more preservation machinery. Enforced at parse, post-merge, local state, and persistence. - **Forced unread:** newest 500 insertion-ordered, touched channels refreshed. - **Persistent agent audiences:** 200-scope LRU. An unchanged-audience touch (including re-initializing an existing scope) refreshes LRU order and persists without advancing the scope's revision or emitting; an already-most-recent touch is a pure no-op (no clone, no write), so render-path re-initialization causes zero storage traffic. - **Self profiles:** newest 8 per relay / 32 globally by `updatedAt`, just-written key always preserved; trim count-gates before parsing payloads so under-cap writes skip the scan entirely. - **Sections:** newest 100 + newest 1,000 assignments, orphans removed; `assignChannel` delete/reinserts the touched channel so a reassignment becomes newest in insertion order and cannot be evicted by the next assignment. **Sort prefs:** 104 groups (100 sections + 4 fixed). - **Feature overrides:** `getOverrides()` filters to current-manifest boolean ids on read only — no write-back from the render-path getter. ## Review-driven revisions - `237f25e4` — three narrow changes from the first adversarial review (no render-path storage write, icon cap aligned to relay constant, count-gated profile trim). - `d864ffb0` — fixes for the two GitHub review findings on `237f25e4`: (P1) mute/star bounding switched from false-tombstone-first eviction to pure recency, with regressions proving an at-capacity unmute/unstar survives bounding and the pre-publish LWW merge; (P2) unchanged agent-audience touches now refresh LRU order (no revision advance, no emit), with a subscriber-mounted regression. - `3ddbb26d` — MRU guard from the second adversarial VERIFY: the P2 touch path skips clone/persist entirely when the scope is already most-recently-inserted, eliminating repeat synchronous localStorage writes from render-path effects. Test proves a non-MRU identical touch writes exactly once (scope persisted last) and an already-MRU touch writes zero times. - `e220ccd9` — fixes for the second GitHub review round (Carl, on Wes's behalf): (1) mute/star bounders preserve the just-mutated key so a same-second mutation at capacity survives its own bounding; merge/sync call sites unchanged; (2) `assignChannel` delete/reinserts the touched key so an at-capacity reassignment isn't evicted by the next new assignment. Regressions at storage and hook level for both; negative-control run of the 7 new tests against the old sources: 7 fail. ## Validation - Full desktop suite 4555/4555 at both `d864ffb0` and `3ddbb26d`, plus desktop-check/typecheck via the push gate; focused storage/audience tests 62/62 at `d864ffb0`, 14/14 audience suite at `3ddbb26d`. - Independent adversarial review: APPROVE at `88a55aee` (including 100 smoke E2E specs covering every seeded store, run manually since push hooks exclude Playwright), then a second VERIFY pass: **VERIFIED at `d864ffb0`** — P1/P2 confirmed closed via negative-control runs of the new suites against the old sources, plus smoke Playwright on the mute/star/audience specs (17 passed). That VERIFY requested one pre-merge change (no localStorage writes from the render path), landed as the narrow MRU guard in `3ddbb26d` within the reviewer's stated no-re-review boundary. A third VERIFY pass: **VERIFIED at `e220ccd9`** — both findings from the second GitHub review confirmed closed by sensitivity testing (new tests fail on old sources), hostile same-call section-trim case constructed and passed, full suite 4562/4562 re-run independently. Authored by Meeseeks (agent), reviewed by Beth (agent), integrated by Rick (agent). Discussion: Buzz channel time-based-localstorage-eviction, thread 0d85a73ca43e54748128f89c3512a4726131bf5473253395d46bf8f3a7b58bd4. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> |
||
|
|
bb9aae1065 |
feat(desktop): time-based sweep for stale localStorage caches (#5453)
Part of #5418 (Phase 1, lane B). ## What Adds a periodic, whitelist-driven TTL sweep for disposable localStorage caches so a desktop session left open for days converges to the same storage state as one restarted nightly. - New `desktop/src/shared/lib/localStorageSweep.ts`: declarative `LOCAL_STORAGE_SWEEP_RULES` table — six repaintable pure-cache prefixes (matching `PURE_CACHE_KEY_PREFIXES` in `localStorageQuota.ts`), all 14-day TTL, keyed on each payload's `updatedAt` (user-label buckets use their newest nested per-profile timestamp). - Entries with no trustworthy timestamp are retained, never guessed stale. `buzz-self-profile.v1:` is deliberately excluded — it is the load-bearing offline identity fallback (guard comment in the table). - Scheduler: first sweep deferred off the boot critical path via `requestIdleCallback` (1.5s timeout) with a 250ms timer fallback, then hourly and on return-to-visible, debounced to 5 minutes. Throw-safe throughout (failures `console.warn`, never crash — per `safeStorage.ts` conventions / #5078). - Wired in `desktop/src/main.tsx` beside `recoverLocalStorageQuotaOnStartup()`. ## Validation - Focused node test 7/7 at HEAD; pre-push gate green (desktop-check, desktop-typecheck, full desktop-test 4542/4542). - Manual Playwright (not covered by push hooks): `relay-connectivity.spec.ts -g "04"` (offline cached identity) passes 1/1 at HEAD — this spec caught and now guards the v1 regression. - Independent adversarial review: FULL REVIEW (REQUEST CHANGES) then VERIFIED — PASS at exactly this commit, including whitelist containment against the 58-site inventory, scheduler tracing, and smoke E2E. Authored by Summer (agent), reviewed by Beth (agent), integrated by Rick (agent). Discussion: Buzz channel time-based-localstorage-eviction, thread 0d85a73ca43e54748128f89c3512a4726131bf5473253395d46bf8f3a7b58bd4. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz> |
||
|
|
43573d114b |
ci(release): gate OSS desktop auto-update promotion (#5398)
## Summary Separate OSS desktop artifact publication from fleet-wide auto-update promotion. - retain the exact generated updater manifest as `updater-manifest.json` on each immutable `desktop-vX.Y.Z` release - stop the tag-triggered build from mutating `buzz-desktop-latest/latest.json` - add a `main`-only manual promotion workflow with one global concurrency group - validate stable semver, release/tag commit identity, draft/prerelease state, exact platform set, signatures, version-bound asset URLs, asset existence, monotonicity, idempotent retries, and a final stale-state check before writing - document the operator flow and pin the split with focused contract tests ## Safety behavior Publishing a versioned GitHub release no longer exposes it through the in-app updater. Operators can install and test those exact signed/notarized artifacts, then manually run **Promote OSS Desktop Auto-Update** with the stable version. Promotion rejects downgrades. A same-version retry succeeds only when the rolling and candidate manifests are byte-identical. The workflow re-reads the current rolling version immediately before its only write and records the actor, source tag commit, previous version, manifest digest, and run URL. ## Verification Verified at commit `39caf1603be06bb476905225ec55f7bbbe86b237`: ```text scripts/test-oss-desktop-promotion.sh OSS desktop promotion contract passed scripts/test-release-ref-contract.sh release ref contract passed git diff --check origin/main...HEAD (clean) ``` The repository pre-push hook also passed `branch-skew` for the exact pushed head; package suites were correctly skipped because this change only touches release workflows, scripts, and documentation. Originating conversation: Buzz channel `separate-publish-step-release`, thread `8857ce8bbe928e891165eddcf06c666cf6eae16181c3f02a6d8c396d8a536026`. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
c1e20a814b |
fix(release): pin desktop PR operations to block/buzz (#5212)
## Summary Pin every GitHub CLI PR operation in the Desktop release helper to `block/buzz`. Without an explicit repository, `gh` refuses to create the release PR in checkouts that have multiple GitHub remotes and no configured default. This happens after the candidate has already been generated, validated, committed, and pushed. Add release-contract assertions covering the list, edit, and create paths so repository qualification cannot regress. ## Validation - `bash -n scripts/prepare-desktop-release.sh scripts/test-release-ref-contract.sh` - `scripts/test-release-ref-contract.sh` - pre-push `branch-skew` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
3c76f682c3 |
fix(search): surface exact short profile names (#5480)
## Summary - prioritize exact whole-lexeme matches within short kind-0 prefix searches - preserve the existing prefix result set, pagination, community/channel scope, hydration, and authorization path - add a Postgres regression where newer noisy `jm…` profiles saturate the bounded page ## Why Desktop mention autocomplete starts searching after one character. The `jm` profile is indexed and matches both `jm:*` prefix search and standard full-text search, but production prefix search returns a full 50-result page without it. Raw profile JSON supplies enough unrelated `jm…` lexemes that newer equal-rank matches fill the bounded page before the exact short display name. Changing clients would leave deployed Desktop 0.5.8 installations broken. This shared search-layer compatibility fix changes ordering only for `Prefix + kinds:[0] + query length <= 2`; message search, longer profile typeahead, and agent eligibility are untouched. ## Validation At commit `ff88761135d5045139aeb3da14d08cbfba203169` with a clean worktree: - `BUZZ_TEST_DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz cargo test -p buzz-search --tests -- --include-ignored` — 22 passed (3 unit + 19 Postgres integration) - `cargo clippy -p buzz-search --tests -- -D warnings` - `cargo fmt --all -- --check` - mutation check: disabling exact-lexeme priority makes `short_kind0_prefix_prioritizes_exact_lexeme_on_a_noisy_page` fail - mandatory pre-push hooks: branch-skew, Rust tests, and Desktop/Tauri checks passed ## Risk Low. The extra ordering predicate applies only to one- or two-character prefix searches restricted exactly to kind 0. It does not add candidates, bypass filters, or alter access control. Exact matches move ahead of broader prefix matches; all remaining ordering stays relevance, recency, then event ID. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
563e4346da |
Reduce repeated ACP session context (#5423)
## Summary - deliver legacy ACP standing context once per live session, committing delivery state only after a successful turn - send only new thread/DM event deltas on later turns, with fail-open behavior for missing IDs and failed/cancelled prompts - fence native steer delivery acknowledgements by ACP session identity so stale acks cannot poison replacement sessions - keep context hints truthful when a fetch contains only the triggering event versus history delivered earlier ## Validation The pre-push hook passed on exact pushed head `6a768f1bc80fe63c686acf8d730f177fff8add3c`: - `branch-skew` - `desktop-check` - `desktop-typecheck` - `desktop-test` - `rust-tests` - `desktop-tauri-checks` Focused regression tests were also run while iterating: - `channel_prompt_commits_delivery_state_only_after_acp_success` - `in_flight_stale_native_steer_ack_cannot_update_replacement_session` - thread/DM trigger-only versus previously-delivered context hint tests ## Known limitations and follow-ups A local Goose smoke timed out at `session/new`. This diff does not change code that executes at or before `session/new`; its earliest affected runtime behavior is delivery-state insertion after session creation succeeds. The smoke failure is therefore bounded as environmental or pre-existing, but no successful live-provider turn was obtained. Scripted ACP wire/lifecycle tests carry the regression coverage. - #5421 — distinguish post-delta, already-delivered, and fetch-truncated context counts - #5422 — define a standing-context re-delivery policy if a legacy provider compacts it away Durable process-restart/session resume remains out of scope for this slice of #5342. #5386 also remains separate pending upstream adapter support. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
5e4c05f90b |
feat(desktop): NIP-AM agent-usage backend — P2 emission/transport/archive + P4a aggregation/D6 (#4000)
## What Implements Phases 2 and 4a of the Usage v2 plan (plan events `d0268cd0`/`0e95b035`), extending the archive backend to emit, transport, archive, and aggregate both cache categories and billing identity fail-closed. ### P2 — emission, transport, archive **Tri-state accumulators** (`Unseen`/`Exact`/`Unknown`) for cache-read and cache-write in `buzz-agent` turn and session state. Absent field = Unknown (never zero) through the full pipeline. No `unwrap_or(0)` on the cache path. Both cache folds are gated on usage-bearing responses (same gate as the total-state and identity folds) — a response with no usage at all must not poison either accumulator. **Overflow-aware input token parsing and accumulation** — closed end-to-end from parse through wire to ACP: - `sum_usage()` returns `SumUsageResult` (`Exact(u64)` | `Overflow`) — checked arithmetic, never clamps. `anthropic_input_tokens()` returns `Option<SumUsageResult>` since it sums three fields (`input_tokens + cache_read_input_tokens + cache_creation_input_tokens`) that can collectively overflow. Single-field callers (`prompt_tokens`, `completion_tokens`, etc.) convert via `.into_exact()` — their single-field sums cannot overflow. - `LlmResponse.input_tokens_overflowed: bool` propagates the parse-layer signal into the run loop. When set, `input_tokens` is `None` (clamped value discarded), the context-gate baseline (`last_request_input_tokens`) is frozen at its prior reading, and `turn_input_tokens` is poisoned to `TurnIOState::Poisoned` before any emission — including mid-turn `emit_usage_update` calls. A dedicated enum on `LlmResponse.input_tokens` would ripple into ~20 existing test assertions on `r.input_tokens == Some(...)`; the bool flag confines the change to the two call sites that check it. - `TurnIOState` (`Unseen`/`Exact`/`Poisoned`) for input and output: per-round fold uses `checked_add`; overflow poisons permanently at turn and session level, no healing. Absence does not poison (pass-2-cleared contract unchanged). Wire emission omits `accumulatedInputTokens`/`accumulatedOutputTokens` when poisoned — never null, never `u64::MAX`. ACP treats absent = publisher-poisoned: `delta_reliable: false`, null turn fields, null cumulative for that category; session cumulative stays unknown for all subsequent turns once poisoned. **Conditional wire emission** for `accumulatedCachedInputTokens` and new `accumulatedCacheWriteTokens`: fields are omitted when the cumulative is Unseen or Unknown. ACP `_goose/unstable/session/update` contract documented next to the payload with tests for all absence/zero variants. **`PricingIdentity` stamping (publisher-side)**: - `pricing_authority()`: canonical parsed-URL endpoint comparison against the official allowlist — HTTPS only, exact allowlisted host (lookalike-safe), default port (omitted or explicit :443), required API base path, rejects userinfo/query/fragment/path-prefix lookalikes. - Model: the actually-requested `request_model` after mesh/auto resolution (not `effective_model_str`). - Turn discipline: identity retained only while ALL usage in the current turn carries one identical proven identity; any mismatch, unproven-usage-bearing response, or unpaired cumulative snapshot poisons to absent; a later matching notification does not heal a mixed turn. **ACP `UsageTracker` identity fold**: per-in-flight-turn tri-state identity accumulator replacing last-update-wins. Any absent identity on a token-advancing notification or exact mismatch poisons to absent; poison survives later updates; reset in `begin_turn()`/`take()`; reset also when a request fails (baseline cleared so preflight gate cannot stay frozen sub-threshold on retries). **M3 migration**: adds `turn_cache_write_tokens`, `cumulative_cache_write_tokens`, `pricing_authority`, `pricing_model`, `pricing_cache_class` to `agent_metric_index`. Additive, idempotent, guarded per-column by marker. M2 migration also guarded per-column (turn and cumulative cache-read columns checked and added independently; marker commits only after both are present). Fresh-DB schema includes all columns. **First-turn baselines**: `seed_zero_baseline` seeds `last_input: Some(0)`, `last_output: Some(0)`, `last_cached_input: Some(0)`, `last_cache_write: Some(0)`, and `last_total: Some(0)` — all have the known-zero-at-spawn argument. Absent fields from incoming snapshots still produce unknown (tri-state unchanged). Sessions buzz-acp did not spawn (no seed) remain fail-closed on turn one. **`ReportedUsage` TS mirror**: `cacheReadTokens`, `cacheWriteTokens`, `freshInputTokens` added to `tauriArchive.ts` as `UsageField` members, field-for-field with the Rust struct. ### P4a — aggregation layer **Extended S-1 ladder** to cache-read and cache-write via the same `ladder_token` path as the existing token fields. **`freshInputTokens` derivation**: checked arithmetic, fail-closed — absent cache fields produce Unknown (not zero), overflow and `cacheRead+cacheWrite > input` both produce `incomplete: true`. Aggregated as a `UsageField`. **D6 comparator**: `sort_value()` = provider total when known, else `input+output` when both known, else `None` (unknown-last). Replaces the prior total-only comparator for both agent-level and model-level sort. Ships a pinned test vector that the TS render layer (P5) must match. ## Test coverage - `buzz-agent`: 440 lib + 15 integration (golden_transcripts) — includes 13 new `cache_total_state_tests`; 14 new `turn_io_state_tests`; 3 new `sum_usage_*` tests (exact single-field, exact two-field, overflow signals correctly); 3 new `parse_anthropic_*` tests (overflow flag set + value cleared, normal sum no flag, absent usage no flag); end-to-end golden transcript drives real subprocess with Anthropic-shaped `input_tokens: u64::MAX, cache_read: 1` response and asserts `accumulatedInputTokens` absent from the emitted `usage_update` — no logic duplication; 3 wire pin tests; 4 `fold_pricing_identity_*` tests; `pricing_authority()` explicit-:443 acceptance - `buzz-acp`: 700 tests (691 lib + 9 integration) — 4 new usage tests (absent input → unreliable+null; absent output → unreliable+null; goose-shaped both present unchanged; poison mid-session); 3 ACP behavior tests; 7 pool lifecycle tests - Desktop (Rust): 2259+ tests — 14 new P4a pinned tests; 2 M3 round-trip tests; 1 serde key-shape test; 2 M2 partial-schema migration tests; first-turn cache round-trip test ## Related PRs - P1 NIP-AM spec: [#4632](https://github.com/block/buzz/pull/4632) - P3 pricing table: [#4629](https://github.com/block/buzz/pull/4629) - UI (P5): [#4001](https://github.com/block/buzz/pull/4001) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
44456e200e |
fix(desktop): resolve overlapping member mentions (#5225)
## Why Selecting or typing a member whose display name extends another member's name, such as `@Fast Fizz Codex`, could emit p-tags for both identities and wake the wrong agent. ## What - Resolve overlapping member-name matches by choosing the longest valid display name at each mention offset - Preserve separately typed short-name mentions at different offsets - Add regression coverage for selected team expansions and manually typed prefix collisions ## Risk Assessment Low to medium — this changes Desktop mention routing only. Exact mentions and distinct offsets remain supported; same-length ambiguous display names remain conservatively tagged because text alone cannot disambiguate them. Will resolve https://github.com/block/buzz/issues/2909 Generated with Goose Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> Signed-off-by: Atish Patel <atish@squareup.com> Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> |
||
|
|
119a84897f |
chore(deps): update react monorepo (#4441)
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@types/react](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react)) | [`19.2.17` → `19.2.18`](https://renovatebot.com/diffs/npm/@types%2freact/19.2.17/19.2.18) |  |  | | [@types/react-dom](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-dom) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom)) | [`19.2.3` → `19.2.4`](https://renovatebot.com/diffs/npm/@types%2freact-dom/19.2.3/19.2.4) |  |  | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4xMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> |
||
|
|
d2ebaa95a7 |
ci(security): allow retired relay pool advisory (#5404)
## Summary - temporarily allow the informational `RUSTSEC-2026-0243` advisory for the retired `nostr-relay-pool` crate - document the exact MeshLLM → `nostr-sdk 0.44.1` transitive path and removal condition - keep every other advisory and the global dependency policy enforced ## Why an exception RustSec provides no patched `nostr-relay-pool` release because the standalone crate was absorbed into `nostr-sdk >= 0.45`. Buzz inherits it through pinned MeshLLM v0.74. A direct test bump to `nostr-sdk 0.45.1` removed the retired crate but produced 13 MeshLLM API compilation errors, so the durable fix requires an upstream source migration rather than a lockfile update. This narrow exception restores the required Security check while that migration is completed. It must be removed once MeshLLM adopts `nostr-sdk >= 0.45`. ## Validation - `bin/cargo-deny --locked check --config deny.toml advisories` - `bin/cargo-deny --locked check` - `git diff --check origin/main...HEAD` - mandatory pre-push Rust and desktop/Tauri checks ## Scope One four-line `deny.toml` addition. No Rust source, lockfile, runtime, or release behavior changes. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
c923e89a4b |
chore(deps): update dependency @tanstack/react-virtual to v3.14.9 (#4439)
This PR contains the following updates:
| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@tanstack/react-virtual](https://tanstack.com/virtual)
([source](https://redirect.github.com/TanStack/virtual/tree/HEAD/packages/react-virtual))
| [`3.14.8` →
`3.14.9`](https://renovatebot.com/diffs/npm/@tanstack%2freact-virtual/3.14.8/3.14.9)
|

|

|
---
> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.
---
### Release Notes
<details>
<summary>TanStack/virtual (@​tanstack/react-virtual)</summary>
###
[`v3.14.9`](https://redirect.github.com/TanStack/virtual/blob/HEAD/packages/react-virtual/CHANGELOG.md#3149)
[Compare
Source](https://redirect.github.com/TanStack/virtual/compare/@tanstack/react-virtual@3.14.8...@tanstack/react-virtual@3.14.9)
##### Patch Changes
- Updated dependencies
\[[`a5417b4`](
|
||
|
|
856cdb848b |
chore(deps): update all non-major dependencies (#3049)
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | Type | Update | |---|---|---|---|---|---| | [@isomorphic-git/lightning-fs](https://redirect.github.com/isomorphic-git/lightning-fs) | [`4.6.2` → `4.6.3`](https://renovatebot.com/diffs/npm/@isomorphic-git%2flightning-fs/4.6.2/4.6.3) |  |  | dependencies | patch | | [@vitejs/plugin-react](https://redirect.github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme) ([source](https://redirect.github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react)) | [`6.0.3` → `6.0.5`](https://renovatebot.com/diffs/npm/@vitejs%2fplugin-react/6.0.3/6.0.5) |  |  | devDependencies | patch | | [@vitejs/plugin-react](https://redirect.github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme) ([source](https://redirect.github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react)) | [`6.0.3` → `6.0.5`](https://renovatebot.com/diffs/npm/@vitejs%2fplugin-react/6.0.3/6.0.5) |  |  | dependencies | patch | | [dorny/paths-filter](https://redirect.github.com/dorny/paths-filter) | `v4.0.2` → `v4.0.3` |  |  | action | patch | | [isomorphic-git](https://isomorphic-git.org/) ([source](https://redirect.github.com/isomorphic-git/isomorphic-git)) | [`1.38.7` → `1.38.10`](https://renovatebot.com/diffs/npm/isomorphic-git/1.38.7/1.38.10) |  |  | dependencies | patch | | [postcss](https://postcss.org/) ([source](https://redirect.github.com/postcss/postcss)) | [`8.5.19` → `8.5.26`](https://renovatebot.com/diffs/npm/postcss/8.5.19/8.5.26) |  |  | devDependencies | patch | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>isomorphic-git/lightning-fs (@​isomorphic-git/lightning-fs)</summary> ### [`v4.6.3`](https://redirect.github.com/isomorphic-git/lightning-fs/releases/tag/v4.6.3) [Compare Source](https://redirect.github.com/isomorphic-git/lightning-fs/compare/v4.6.2...v4.6.3) ##### Bug Fixes - IDB interface ([#​127](https://redirect.github.com/isomorphic-git/lightning-fs/issues/127)) ([035e472]( |
||
|
|
e1ff91ecc1 |
chore(deps): update rust crate anyhow to v1.0.104 (#4447)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [anyhow](https://redirect.github.com/dtolnay/anyhow) | dependencies | patch | `1.0.103` → `1.0.104` | | [anyhow](https://redirect.github.com/dtolnay/anyhow) | workspace.dependencies | patch | `1.0.103` → `1.0.104` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>dtolnay/anyhow (anyhow)</summary> ### [`v1.0.104`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.104) [Compare Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.103...1.0.104) - Update `syn` dev-dependency to version 3 </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4zLjIiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |