diff --git a/.env.example b/.env.example index db5a7ea25..3dc54856e 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,10 @@ REDIS_URL=redis://localhost:6379 # Max connections in the relay's shared Redis pool (default 16). # BUZZ_REDIS_POOL_SIZE=16 +# Max connections in each of the relay's Postgres pools — writer and, when +# READ_DATABASE_URL is set, reader (default 50). +# BUZZ_DB_POOL_SIZE=50 + # ----------------------------------------------------------------------------- # Typesense (search) # ----------------------------------------------------------------------------- @@ -105,7 +109,10 @@ RELAY_URL=ws://localhost:3000 # ----------------------------------------------------------------------------- # Logging / Tracing # ----------------------------------------------------------------------------- -RUST_LOG=buzz_relay=debug,buzz_db=debug,buzz_auth=debug,buzz_pubsub=debug,tower_http=debug +RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz_pubsub=debug,tower_http=debug +# Optional OpenTelemetry-only target filter. This is deliberately independent +# from RUST_LOG so log verbosity changes cannot break trace parentage. +# BUZZ_OTEL_FILTER=buzz_relay=info,buzz_datastore=info # OTLP tracing endpoint (optional — leave unset to disable) # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 31080652e..52f21b28b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,6 +1,8 @@ name: Docker image -# Builds and publishes the public Buzz relay image as ghcr.io/block/buzz. +# Builds and publishes the public Buzz relay images as ghcr.io/block/buzz. +# Normal tags contain stripped binaries; matching debug-* tags contain the same +# optimized binaries with line-table debug information for native profilers. # # Strategy: each architecture builds on its native runner (ubuntu-24.04 for # amd64, ubuntu-24.04-arm for arm64), pushes to GHCR by digest, then a final @@ -15,8 +17,10 @@ name: Docker image # # Triggers: # - push to main → :main + :sha-<7> +# + :debug-main + :debug-sha-<7> # - push tags relay-v*.*.* → :{version} + :{major}.{minor} + :{major} -# (+ :latest for stable, NOT for prereleases) +# + matching :debug-* tags +# (+ :latest/:debug-latest for stable releases) # - pull_request → build only (no push), cache stays warm # - workflow_dispatch → manual relay-tag rescue at the tag itself # @@ -95,10 +99,6 @@ jobs: runner: ubuntu-24.04-arm arch: arm64 - outputs: - # Used downstream by `merge` to stitch the manifest. - version: ${{ steps.meta.outputs.version }} - steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -163,12 +163,13 @@ jobs: org.opencontainers.image.description=WebSocket relay server for the Buzz communications platform org.opencontainers.image.licenses=Apache-2.0 - - name: Build and push by digest - id: build + - name: Build and push release image by digest + id: build-release uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: ./Dockerfile + target: runtime platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} # Push by digest, not by tag — the merge job assembles the tags @@ -180,25 +181,49 @@ jobs: cache-to: | ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}-buildcache:{1},mode=max,compression=zstd', env.IMAGE_NAME, matrix.arch) || '' }} - - name: Export digest + - name: Build and push debug image by digest + id: build-debug + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: . + file: ./Dockerfile + target: runtime-debug + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + cache-from: | + type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }} + + - name: Export release and debug digests if: github.event_name != 'pull_request' env: - DIGEST: ${{ steps.build.outputs.digest }} + RELEASE_DIGEST: ${{ steps.build-release.outputs.digest }} + DEBUG_DIGEST: ${{ steps.build-debug.outputs.digest }} run: | - mkdir -p /tmp/digests - touch "/tmp/digests/${DIGEST#sha256:}" + mkdir -p /tmp/digests-release /tmp/digests-debug + touch "/tmp/digests-release/${RELEASE_DIGEST#sha256:}" + touch "/tmp/digests-debug/${DEBUG_DIGEST#sha256:}" - - name: Upload digest + - name: Upload release digest if: github.event_name != 'pull_request' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: digests-${{ matrix.arch }} - path: /tmp/digests/* + name: digests-release-${{ matrix.arch }} + path: /tmp/digests-release/* + if-no-files-found: error + retention-days: 1 + + - name: Upload debug digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: digests-debug-${{ matrix.arch }} + path: /tmp/digests-debug/* if-no-files-found: error retention-days: 1 merge: - name: Merge multi-arch manifest + name: Merge ${{ matrix.variant }} multi-arch manifest if: github.event_name != 'pull_request' runs-on: ubuntu-24.04 needs: build @@ -208,13 +233,21 @@ jobs: packages: write # push the merged manifest id-token: write # OIDC for provenance attestation on the manifest attestations: write + strategy: + fail-fast: false + matrix: + include: + - variant: release + tag_prefix: "" + - variant: debug + tag_prefix: debug- steps: - name: Download all per-arch digests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: /tmp/digests - pattern: digests-* + pattern: digests-${{ matrix.variant }}-* merge-multiple: true - name: Set up Docker Buildx @@ -237,9 +270,12 @@ jobs: # the build job's `meta` step for why match=^relay-v(.*)$, why # value=${{ inputs.version }} carries the rescue-dispatch version, # and why :latest is left to flavor.latest=auto. + flavor: | + latest=auto + prefix=${{ matrix.tag_prefix }},onlatest=true tags: | type=ref,event=branch,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }} - type=sha,prefix=sha-,format=short,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }} + type=sha,prefix=${{ matrix.tag_prefix }}sha-,format=short,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }} type=semver,pattern={{version}},match=^relay-v(.*)$,value=${{ inputs.version }} type=semver,pattern={{major}}.{{minor}},match=^relay-v(.*)$,value=${{ inputs.version }} type=semver,pattern={{major}},match=^relay-v(.*)$,value=${{ inputs.version }} @@ -284,11 +320,12 @@ jobs: - name: Summary env: IMAGE_NAME: ${{ env.IMAGE_NAME }} + VARIANT: ${{ matrix.variant }} MERGED_DIGEST: ${{ steps.manifest.outputs.digest }} META_TAGS: ${{ steps.meta.outputs.tags }} run: | { - echo "### Published \`${IMAGE_NAME}\`" + echo "### Published \`${IMAGE_NAME}\` (${VARIANT})" echo echo "**Digest:** \`${MERGED_DIGEST}\`" echo diff --git a/CHANGELOG.md b/CHANGELOG.md index 122393650..cfd3b16d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # Changelog +## v0.5.0 + +- feat(invites): add use-limited invite links ([#3141](https://github.com/block/buzz/pull/3141)) ([`d500c2d5c`](https://github.com/block/buzz/commit/d500c2d5cf5d9aabe0ca4ebebfcafdbe5f5b7fd3)) +- fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor ([#3218](https://github.com/block/buzz/pull/3218)) ([`98a7b1334`](https://github.com/block/buzz/commit/98a7b1334823ee0be3e3fa5cab7a2e349e438dab)) +- fix(desktop): preserve thread anchor through layout reflow ([#3212](https://github.com/block/buzz/pull/3212)) ([`9810d8545`](https://github.com/block/buzz/commit/9810d8545937329f229ff40d8a19edc9e3e325c1)) +- feat(search): parse from:/in:/after:/before: and pass them in the filter ([#2871](https://github.com/block/buzz/pull/2871)) ([`cb2a265b5`](https://github.com/block/buzz/commit/cb2a265b5399426e808461c1a16713754c593258)) +- fix(desktop): fetch join policies through native networking ([#2862](https://github.com/block/buzz/pull/2862)) ([`0019f8076`](https://github.com/block/buzz/commit/0019f80765e96f056e81b57789b8b5fb80936f72)) +- fix(desktop): republish agent identity records when a persona rename propagates ([#2607](https://github.com/block/buzz/pull/2607)) ([`7ca0bbd94`](https://github.com/block/buzz/commit/7ca0bbd946fd82a7008132f94d069a97bb53f94b)) +- fix(desktop): keep project Inbox previews compact ([#3193](https://github.com/block/buzz/pull/3193)) ([`de1396050`](https://github.com/block/buzz/commit/de13960505fd798070e177cb33b1663100ac06bb)) +- Inbox refactor ([#2045](https://github.com/block/buzz/pull/2045)) ([`2bd4c24b7`](https://github.com/block/buzz/commit/2bd4c24b71335e7ce272ec6de6491f7f37f4b20d)) +- Fix composer selection formatting and drop overlay ([#3172](https://github.com/block/buzz/pull/3172)) ([`99da5b7eb`](https://github.com/block/buzz/commit/99da5b7ebb19e26453e075bfb949672122b31be3)) +- Refine pending message status ([#3153](https://github.com/block/buzz/pull/3153)) ([`75588eaff`](https://github.com/block/buzz/commit/75588eaff2354d620e554c055b80ec83735ddb0a)) +- fix(desktop): recover full local storage on startup ([#3182](https://github.com/block/buzz/pull/3182)) ([`174c38e4b`](https://github.com/block/buzz/commit/174c38e4bd1ed8498641546bc4fcb6d5a4c9cede)) +- fix(desktop): keep collapsed table separators out of spoilers ([#3169](https://github.com/block/buzz/pull/3169)) ([`4d8b676bb`](https://github.com/block/buzz/commit/4d8b676bb283a1917cec5850c3b7327fe122b0c1)) +- feat(desktop): redesign agent runtime settings ([#3093](https://github.com/block/buzz/pull/3093)) ([`d98da7389`](https://github.com/block/buzz/commit/d98da7389e60cfbd79b219aa411449fe2e53a18a)) +- fix(desktop): use forward slashes for git credential.helper on Windows ([#3023](https://github.com/block/buzz/pull/3023)) ([`899531684`](https://github.com/block/buzz/commit/8995316844f7ad50552fbae67fbd35119262796f)) +- chore(desktop): add AgentCreationPreview file-size override to unblock main CI ([#3154](https://github.com/block/buzz/pull/3154)) ([`b92a1f4bf`](https://github.com/block/buzz/commit/b92a1f4bf400e7da5ab7a010cdd81a69497d8191)) +- fix(desktop): make the test loader work on Windows ([#2758](https://github.com/block/buzz/pull/2758)) ([`8bb43d519`](https://github.com/block/buzz/commit/8bb43d51912894553f2670b2d285a96cf09cd472)) +- fix(desktop): make lint and unit-test gates work on Windows ([#2943](https://github.com/block/buzz/pull/2943)) ([`545bb46b8`](https://github.com/block/buzz/commit/545bb46b824a3fbf4401062f03b72531d832ebb9)) +- feat(desktop): add search to agent emoji picker ([#2630](https://github.com/block/buzz/pull/2630)) ([`313f793c8`](https://github.com/block/buzz/commit/313f793c8753d413c22ff8edfe420d5ee78708bc)) +- fix(desktop): keep identity key help dialog readable in dark mode ([#2854](https://github.com/block/buzz/pull/2854)) ([`be275cfc6`](https://github.com/block/buzz/commit/be275cfc6c7b80fe43e9d66c6d14b6d2bbe58a10)) +- feat(acp): title agent sessions from the agent and channel name ([#3028](https://github.com/block/buzz/pull/3028)) ([`f2fe3b63c`](https://github.com/block/buzz/commit/f2fe3b63c21be55907175715c076cd3a9195b74d)) +- feat(git): use agent display name as git author name ([#3040](https://github.com/block/buzz/pull/3040)) ([`18eef633d`](https://github.com/block/buzz/commit/18eef633d88ac465c61d98f12655fbf51dc3ca44)) +- fix(deps): bump nostr to 0.44.6 for RUSTSEC-2026-0216 (NIP-44 remote DoS) ([#3135](https://github.com/block/buzz/pull/3135)) ([`31e2de196`](https://github.com/block/buzz/commit/31e2de1966672e73e026af3c54f3a1a9a2f5e103)) +- fix(desktop): read the newest pair-scoped harness log ([#3134](https://github.com/block/buzz/pull/3134)) ([`654f38490`](https://github.com/block/buzz/commit/654f384906b5c720a60a199d85031a6f1cb6efc9)) +- feat(desktop): handle project work from Inbox ([#3117](https://github.com/block/buzz/pull/3117)) ([`c5c4f390b`](https://github.com/block/buzz/commit/c5c4f390b6713256e2efb8394c59823ebad73db6)) +- fix(desktop): clarify identity key button when key exists ([#2357](https://github.com/block/buzz/pull/2357)) ([`87b3fcd3c`](https://github.com/block/buzz/commit/87b3fcd3c0131683569dd4268b099d18b25dcd5e)) +- Restore Goose and Buzz Agent to onboarding harness selection ([#2731](https://github.com/block/buzz/pull/2731)) ([`7fc0cc82d`](https://github.com/block/buzz/commit/7fc0cc82db4d9dced9c258bbe8b530164a832a77)) +- fix(desktop): render rich project work item content ([#3100](https://github.com/block/buzz/pull/3100)) ([`afb272bb7`](https://github.com/block/buzz/commit/afb272bb7b8d7d45d7de676fa97dcd5a8eefacc7)) +- feat(acp): bring your own harness (BYOH) — generic ACP runtime seam + settings gallery ([#2773](https://github.com/block/buzz/pull/2773)) ([`95fdf9788`](https://github.com/block/buzz/commit/95fdf978800982389b120c66ff5e766d785419c7)) +- feat(desktop): use collective mesh routing for Auto ([#2825](https://github.com/block/buzz/pull/2825)) ([`16d4ec335`](https://github.com/block/buzz/commit/16d4ec335e210295a9d9f77f36c1e85a18b6814a)) +- fix(desktop): strip legacy baked team instructions from stored prompts ([#3035](https://github.com/block/buzz/pull/3035)) ([`aee631448`](https://github.com/block/buzz/commit/aee63144843854ee32ed9d36a2e7511c82ddc6b0)) +- feat(agents): lower default agent parallelism from 24 to 10 ([#3038](https://github.com/block/buzz/pull/3038)) ([`5d8ede446`](https://github.com/block/buzz/commit/5d8ede446f8fdc48146fe56d389cab6bf3500f92)) +- Polish community rail and mobile pairing ([#2972](https://github.com/block/buzz/pull/2972)) ([`e6c90bb7c`](https://github.com/block/buzz/commit/e6c90bb7c430d1b2af16508b634f9a5283b7fa3b)) +- fix(desktop): remove bundled libsystemd from AppImage ([#2353](https://github.com/block/buzz/pull/2353)) ([`a31fc4d2f`](https://github.com/block/buzz/commit/a31fc4d2f35d51cdf45ff8c61fc3a07f49c665e8)) +- fix(desktop): make agent definition authoritative for model/provider/prompt ([#1968](https://github.com/block/buzz/pull/1968)) ([`8c0e8cb16`](https://github.com/block/buzz/commit/8c0e8cb1656b04ad269bce3c2deeda2a943ae78a)) +- chore(desktop): delete dead persona catalog UI cluster ([#2886](https://github.com/block/buzz/pull/2886)) ([`8e67cf399`](https://github.com/block/buzz/commit/8e67cf399d0291bcdbc69cd0402983ca030f05bb)) +- fix(desktop): surface install failures hidden by curl-pipe exit codes ([#2892](https://github.com/block/buzz/pull/2892)) ([`166c6655e`](https://github.com/block/buzz/commit/166c6655e8bca87d83ad60c087fb70a32a026baf)) +- Refactor managed-agent runtime into cohesive modules ([#2974](https://github.com/block/buzz/pull/2974)) ([`74b63e184`](https://github.com/block/buzz/commit/74b63e1846212af6e6751a62cfc631f74b1dfe07)) +- fix(desktop): make Linux AppImage GStreamer work on non-Debian distros ([#2176](https://github.com/block/buzz/pull/2176)) ([`cc6c4d347`](https://github.com/block/buzz/commit/cc6c4d3471629fad018bcf645f9471a01b9ffe2f)) +- refactor(desktop): remove Agent directory section from Agents page ([#2290](https://github.com/block/buzz/pull/2290)) ([`5d1233e84`](https://github.com/block/buzz/commit/5d1233e841b0efa91470bb45467b2c8e4284ebf6)) +- fix(desktop): enable arboard Wayland backend so Linux copies reach the Wayland clipboard ([#2904](https://github.com/block/buzz/pull/2904)) ([`ab7aa8b12`](https://github.com/block/buzz/commit/ab7aa8b1200710dbc2d7a8661ed5aab95c4199c1)) +- fix(desktop): supervise and re-arm relay-mesh runtime ([#2823](https://github.com/block/buzz/pull/2823)) ([`aa51dab9d`](https://github.com/block/buzz/commit/aa51dab9da5fef7054d03cf1a1207986d0000684)) +- fix(agents): run live Databricks discovery instead of the fallback list ([#2890](https://github.com/block/buzz/pull/2890)) ([`8eb6e3eb6`](https://github.com/block/buzz/commit/8eb6e3eb601174249642373a6a367262fa476753)) +- fix(desktop): retire prepend mode on every reader wheel ([#2913](https://github.com/block/buzz/pull/2913)) ([`07d0265cf`](https://github.com/block/buzz/commit/07d0265cfc212ef02e1c26153bf58ff46ce5ffe6)) +- fix(desktop): consolidate prepend scroll correction ([#2855](https://github.com/block/buzz/pull/2855)) ([`25e7864b3`](https://github.com/block/buzz/commit/25e7864b35f4dfd1c0ff31304a38555230a85f8d)) +- fix(desktop): track concurrent agent turns up to the harness maximum ([#2882](https://github.com/block/buzz/pull/2882)) ([`20bff5910`](https://github.com/block/buzz/commit/20bff591023daffc5ee1032cff02b54b75da3567)) +- fix(relay): preserve reconnect backoff ([#2759](https://github.com/block/buzz/pull/2759)) ([`499c5d349`](https://github.com/block/buzz/commit/499c5d349dab13bc906b1af5fe1fcb09ce2afa81)) +- refactor(relay): expose reconnect timing policy ([#2310](https://github.com/block/buzz/pull/2310)) ([`2f0041595`](https://github.com/block/buzz/commit/2f0041595d72529c06885680d2bd07ddb6a0beb4)) +- fix(desktop): clear stale working badges on agent stop/restart ([#2803](https://github.com/block/buzz/pull/2803)) ([`a64cc71f6`](https://github.com/block/buzz/commit/a64cc71f6c1605279b1a6fbd0fe904a2984cbdb0)) +- fix(desktop): surface agent rename relay profile sync failure as a warning toast ([#2279](https://github.com/block/buzz/pull/2279)) ([`5e3d2e484`](https://github.com/block/buzz/commit/5e3d2e4849c0f2512330801d804fb96f4ab72d28)) +- fix(discovery): inject PATH into Codex adapter planning ([#2767](https://github.com/block/buzz/pull/2767)) ([`6ab3835f3`](https://github.com/block/buzz/commit/6ab3835f3fe89ee215819fe8d193463c0ae7472b)) + + ## v0.4.26 - Style mobile pairing QR codes ([#2775](https://github.com/block/buzz/pull/2775)) ([`50655ac09`](https://github.com/block/buzz/commit/50655ac097fbf1a7db1a5284dccc7e2a0b0f1bfc)) diff --git a/Dockerfile b/Dockerfile index 661be6c3a..d883ac6b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,6 +59,9 @@ RUN apt-get update \ ca-certificates \ git \ && rm -rf /var/lib/apt/lists/* +# Keep enough DWARF for native profilers to resolve optimized code to source +# locations. The normal runtime strips it below; runtime-debug retains it. +ENV CARGO_PROFILE_RELEASE_DEBUG=line-tables-only COPY --from=planner /build/recipe.json recipe.json # Cook the full workspace recipe — relay deps include workspace siblings, so # scoping to -p buzz-relay misses transitive deps and re-builds them later. @@ -66,8 +69,12 @@ RUN cargo chef cook --release --recipe-path recipe.json COPY . . RUN cargo build --release --locked -p buzz-relay --bin buzz-relay \ -p buzz-admin --bin buzz-admin \ - -p buzz-pair-relay --bin buzz-pair-relay \ - && strip target/release/buzz-relay \ + -p buzz-pair-relay --bin buzz-pair-relay + +# Derive the normal release binaries from the same optimized ELF files as the +# debug image so the two variants cannot drift at code-generation time. +FROM builder AS stripped-binaries +RUN strip target/release/buzz-relay \ && strip target/release/buzz-admin \ && strip target/release/buzz-pair-relay @@ -111,8 +118,8 @@ COPY web/ web/ COPY admin-web/ admin-web/ RUN pnpm -C web build && pnpm -C admin-web build -# ─── Stage 5: runtime ─────────────────────────────────────────────────────── -FROM debian:${DEBIAN_VERSION}-slim AS runtime +# ─── Stage 5: shared runtime ──────────────────────────────────────────────── +FROM debian:${DEBIAN_VERSION}-slim AS runtime-base # OCI annotations: required for GHCR to auto-link the image to this repo and # inherit its visibility. org.opencontainers.image.source is the load-bearing @@ -135,9 +142,6 @@ RUN apt-get update \ && useradd --system --uid 1000 --gid 1000 --home-dir /var/lib/buzz \ --create-home --shell /usr/sbin/nologin buzz -COPY --from=builder /build/target/release/buzz-relay /usr/local/bin/buzz-relay -COPY --from=builder /build/target/release/buzz-admin /usr/local/bin/buzz-admin -COPY --from=builder /build/target/release/buzz-pair-relay /usr/local/bin/buzz-pair-relay COPY --from=web-builder /build/web/dist /srv/buzz/web COPY --from=web-builder /build/admin-web/dist /srv/buzz/admin-web @@ -157,3 +161,18 @@ USER buzz:buzz WORKDIR /var/lib/buzz ENTRYPOINT ["/usr/local/bin/buzz-relay"] + +# Optimized binaries with line-table debug information for native profiling. +# Published under debug-* tags; runtime behavior otherwise matches the normal +# image exactly. +FROM runtime-base AS runtime-debug +COPY --from=builder /build/target/release/buzz-relay /usr/local/bin/buzz-relay +COPY --from=builder /build/target/release/buzz-admin /usr/local/bin/buzz-admin +COPY --from=builder /build/target/release/buzz-pair-relay /usr/local/bin/buzz-pair-relay + +# Keep the stripped runtime as the final/default Dockerfile target so existing +# `docker build .` callers and release tags retain their current behavior. +FROM runtime-base AS runtime +COPY --from=stripped-binaries /build/target/release/buzz-relay /usr/local/bin/buzz-relay +COPY --from=stripped-binaries /build/target/release/buzz-admin /usr/local/bin/buzz-admin +COPY --from=stripped-binaries /build/target/release/buzz-pair-relay /usr/local/bin/buzz-pair-relay diff --git a/RELEASING.md b/RELEASING.md index 9785122aa..063b813e2 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -57,10 +57,13 @@ or mobile GitHub Release. 2. **Merge the PR.** `auto-tag-on-release-pr-merge` pushes `relay-v`. 3. **The tag triggers `docker.yml`.** Stable releases update the version - aliases and `latest`; prereleases do not. + aliases and `latest`; prereleases do not. Each release also publishes an + optimized, symbol-bearing image under matching `debug-` tags (for example, + `debug-0.3.0` and `debug-latest`) for native profiling. The ordinary tags + remain stripped and are the default for deployments that do not need it. Every push to `main` continues to publish the rolling relay `:main` and -`:sha-<7>` tags. +`:sha-<7>` tags, plus matching `:debug-main` and `:debug-sha-<7>` variants. ### Mobile diff --git a/crates/buzz-acp/src/acp.rs b/crates/buzz-acp/src/acp.rs index 49e4d6cce..c0147baf1 100644 --- a/crates/buzz-acp/src/acp.rs +++ b/crates/buzz-acp/src/acp.rs @@ -1854,7 +1854,8 @@ pub enum ModelSwitchMethod { /// Extract `configOptions` entries with `category == "model"` from a `session/new` result. /// -/// Returns the raw JSON array entries. Each entry has `configId`, `displayName`, +/// Returns the raw JSON array entries. Each entry has `configId` (spelled `id` +/// by some adapters, e.g. claude-agent-acp), `displayName`, /// `options: [{ value, displayName }]`, etc. pub fn extract_model_config_options(result: &serde_json::Value) -> Vec { result["configOptions"] @@ -1888,7 +1889,14 @@ pub fn resolve_model_switch_method( // 1. Search stable configOptions for a "model"-category entry whose // options contain a value matching desired_model. for config_opt in extract_model_config_options(session_new_result) { - let config_id = match config_opt.get("configId").and_then(|v| v.as_str()) { + // Adapters disagree on the key: the ACP spec says `configId`, but + // claude-agent-acp emits `id`. Accept both; the set request always + // uses `configId` on the wire. + let config_id = match config_opt + .get("configId") + .or_else(|| config_opt.get("id")) + .and_then(|v| v.as_str()) + { Some(id) => id, None => continue, }; @@ -2464,6 +2472,36 @@ mod tests { ); } + #[test] + fn resolve_accepts_id_keyed_config_options() { + // claude-agent-acp (observed on v0.61.0) keys config options with + // `id` instead of the spec's `configId`. Payload mirrors its real + // `session/new` response. + let result = serde_json::json!({ + "configOptions": [{ + "id": "model", + "name": "Model", + "category": "model", + "type": "select", + "currentValue": "default", + "options": [ + { "value": "default", "name": "Default" }, + { "value": "opus[1m]", "name": "Opus" }, + { "value": "sonnet", "name": "Sonnet" } + ] + }], + "models": null + }); + let method = super::resolve_model_switch_method(&result, "opus[1m]"); + assert_eq!( + method, + Some(super::ModelSwitchMethod::ConfigOption { + config_id: "model".to_string(), + option_value: "opus[1m]".to_string(), + }) + ); + } + #[test] fn resolve_falls_back_to_unstable() { let result = serde_json::json!({ diff --git a/crates/buzz-relay/Cargo.toml b/crates/buzz-relay/Cargo.toml index 10a83f032..01f78a2d4 100644 --- a/crates/buzz-relay/Cargo.toml +++ b/crates/buzz-relay/Cargo.toml @@ -92,3 +92,4 @@ reqwest = { workspace = true } tokio-tungstenite = { workspace = true } futures = "0.3" flate2 = "1.1.9" +opentelemetry_sdk = { workspace = true, features = ["testing"] } diff --git a/crates/buzz-relay/src/config.rs b/crates/buzz-relay/src/config.rs index 47030dcf3..a1691349d 100644 --- a/crates/buzz-relay/src/config.rs +++ b/crates/buzz-relay/src/config.rs @@ -64,6 +64,14 @@ pub struct Config { /// pod is only 4 — small enough that rate-limit checks, presence, and /// pub/sub publishes queue behind each other under load. pub redis_pool_size: usize, + /// Maximum connections in the Postgres writer/reader pools. Defaults to 50. + /// + /// The `buzz-db` default of 20 was sized for a handful of pods against + /// `max_connections=100`. Against Aurora (~5,000 connections) that cap + /// is the binding constraint: a burst of concurrent handlers exhausts + /// the per-pod pool and requests fail on acquire timeout while the + /// database sits idle. + pub db_pool_size: u32, /// Public WebSocket URL of this relay, advertised in NIP-11. pub relay_url: String, /// Public WebSocket URL of the dedicated device-pairing relay, when configured. @@ -424,6 +432,12 @@ impl Config { .filter(|&v| v > 0) .unwrap_or(16); + let db_pool_size = std::env::var("BUZZ_DB_POOL_SIZE") + .ok() + .and_then(|v| v.parse::().ok()) + .filter(|&v| v > 0) + .unwrap_or(50); + let relay_url = std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string()); @@ -875,6 +889,7 @@ impl Config { read_database_url, redis_url, redis_pool_size, + db_pool_size, relay_url, pairing_relay_url, max_connections, @@ -942,6 +957,7 @@ mod tests { assert!(!config.database_url.is_empty()); assert!(!config.redis_url.is_empty()); assert_eq!(config.redis_pool_size, 16); + assert_eq!(config.db_pool_size, 50); assert!(config.max_connections > 0); assert!(config.send_buffer_size > 0); assert_eq!(config.max_frame_bytes, DEFAULT_MAX_FRAME_BYTES); @@ -1009,6 +1025,31 @@ mod tests { assert_eq!(junk, 16, "unparsable value must fall back to the default"); } + #[test] + fn db_pool_size_env_override_and_invalid_fallback() { + let _guard = ENV_MUTEX.lock().unwrap(); + let previous = std::env::var_os("BUZZ_DB_POOL_SIZE"); + + std::env::set_var("BUZZ_DB_POOL_SIZE", "80"); + let overridden = Config::from_env().expect("config").db_pool_size; + + std::env::set_var("BUZZ_DB_POOL_SIZE", "0"); + let zero = Config::from_env().expect("config").db_pool_size; + + std::env::set_var("BUZZ_DB_POOL_SIZE", "not-a-number"); + let junk = Config::from_env().expect("config").db_pool_size; + + if let Some(value) = previous { + std::env::set_var("BUZZ_DB_POOL_SIZE", value); + } else { + std::env::remove_var("BUZZ_DB_POOL_SIZE"); + } + + assert_eq!(overridden, 80); + assert_eq!(zero, 50, "zero must fall back to the default"); + assert_eq!(junk, 50, "unparsable value must fall back to the default"); + } + #[test] fn read_database_url_unset_or_blank_is_none() { let _guard = ENV_MUTEX.lock().unwrap(); diff --git a/crates/buzz-relay/src/main.rs b/crates/buzz-relay/src/main.rs index 533428a4e..3ed820d3c 100644 --- a/crates/buzz-relay/src/main.rs +++ b/crates/buzz-relay/src/main.rs @@ -4,6 +4,10 @@ use std::sync::Arc; use tracing::{error, info, warn}; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +fn log_env_filter(rust_log: Option<&str>) -> EnvFilter { + EnvFilter::new(rust_log.unwrap_or("buzz_relay=info")) +} use uuid::Uuid; use buzz_audit::AuditService; @@ -107,9 +111,17 @@ async fn main() -> anyhow::Result<()> { }; tracing_subscriber::registry() - .with(fmt::layer().json().flatten_event(true)) - .with(EnvFilter::from_default_env().add_directive("buzz_relay=info".parse()?)) - .with(otel_layer) + .with( + fmt::layer() + .json() + .flatten_event(true) + .with_filter(log_env_filter(std::env::var("RUST_LOG").ok().as_deref())), + ) + .with(otel_layer.map(|layer| { + layer.with_filter(telemetry::otel_env_filter( + std::env::var("BUZZ_OTEL_FILTER").ok().as_deref(), + )) + })) .init(); // Log any exporter-build failure now that the subscriber is installed. @@ -146,6 +158,7 @@ async fn main() -> anyhow::Result<()> { let db_config = DbConfig { database_url: config.database_url.clone(), read_database_url: config.read_database_url.clone(), + max_connections: config.db_pool_size, ..DbConfig::default() }; let db = Db::new(&db_config).await.map_err(|e| { @@ -1060,6 +1073,52 @@ async fn main() -> anyhow::Result<()> { Ok(()) } +#[cfg(test)] +mod env_filter_tests { + use super::log_env_filter; + use buzz_relay::telemetry::otel_env_filter; + use tracing_subscriber::prelude::*; + + #[test] + fn unset_enables_datastore_only_for_otel_filter() { + let logs = tracing_subscriber::registry().with(log_env_filter(None)); + tracing::subscriber::with_default(logs, || { + assert!(!tracing::enabled!(target: "buzz_datastore", tracing::Level::INFO)); + assert!(tracing::enabled!(target: "buzz_relay", tracing::Level::INFO)); + }); + + let otel = tracing_subscriber::registry().with(otel_env_filter(None)); + tracing::subscriber::with_default(otel, || { + assert!(tracing::enabled!(target: "buzz_datastore", tracing::Level::INFO)); + }); + } + + #[test] + fn explicit_datastore_off_is_preserved_alone() { + assert_eq!( + otel_env_filter(Some("buzz_datastore=off")).to_string(), + "buzz_datastore=off" + ); + } + + #[test] + fn explicit_datastore_debug_is_preserved_alone() { + assert_eq!( + otel_env_filter(Some("buzz_datastore=debug")).to_string(), + "buzz_datastore=debug" + ); + } + + #[test] + fn log_and_otel_filters_are_configured_independently() { + assert_eq!(log_env_filter(Some("warn")).to_string(), "warn"); + assert_eq!( + otel_env_filter(Some("buzz_relay=debug")).to_string(), + "buzz_relay=debug" + ); + } +} + async fn run_community_revalidator( state: Arc, period: std::time::Duration, diff --git a/crates/buzz-relay/src/router.rs b/crates/buzz-relay/src/router.rs index 2af036079..400ed1dfe 100644 --- a/crates/buzz-relay/src/router.rs +++ b/crates/buzz-relay/src/router.rs @@ -4,8 +4,9 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use axum::{ + body::Body, extract::{ConnectInfo, FromRequest, State, WebSocketUpgrade}, - http::{HeaderMap, StatusCode}, + http::{HeaderMap, Request, StatusCode}, middleware, response::{IntoResponse, Json}, routing::{get, post, put}, @@ -16,7 +17,7 @@ use tower::ServiceExt; use tower_http::cors::{AllowOrigin, CorsLayer}; use tower_http::limit::RequestBodyLimitLayer; use tower_http::services::ServeDir; -use tower_http::trace::TraceLayer; +use tower_http::trace::{HttpMakeClassifier, TraceLayer}; use crate::api; use crate::audio; @@ -187,10 +188,23 @@ pub fn build_router(state: Arc) -> Router { merged .layer(middleware::from_fn(track_metrics)) - .layer(TraceLayer::new_for_http()) + .layer(http_trace_layer()) .layer(build_cors_layer(&state.config.cors_origins)) } +fn http_trace_layer() -> TraceLayer) -> tracing::Span> { + TraceLayer::new_for_http().make_span_with(make_http_span as fn(&Request) -> tracing::Span) +} + +fn make_http_span(request: &Request) -> tracing::Span { + tracing::info_span!( + target: "buzz_relay", + "http.request", + otel.kind = "server", + http.request.method = %request.method(), + ) +} + fn is_admin_spa_path(path: &str) -> bool { path == "/" || path == "/reports" @@ -435,9 +449,14 @@ fn build_cors_layer(cors_origins: &[String]) -> CorsLayer { mod tests { use axum::{routing::get, Router}; use futures_util::SinkExt; + use opentelemetry::trace::TracerProvider as _; + use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider}; use tokio::net::TcpListener; use tokio::sync::mpsc; use tokio_tungstenite::{connect_async, tungstenite::Message}; + use tower::ServiceBuilder; + use tracing::Instrument as _; + use tracing_subscriber::prelude::*; use super::*; @@ -471,6 +490,60 @@ mod tests { assert!(!should_serve_spa("/arbitrary", true)); } + #[tokio::test(flavor = "current_thread")] + async fn http_and_datastore_spans_are_exported_in_the_same_trace() { + let exporter = InMemorySpanExporter::default(); + let provider = SdkTracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let subscriber = tracing_subscriber::registry().with( + tracing_opentelemetry::layer() + .with_tracer(provider.tracer("test")) + .with_filter(crate::telemetry::otel_env_filter(None)), + ); + let _subscriber_guard = tracing::subscriber::set_default(subscriber); + let service = ServiceBuilder::new() + .layer(http_trace_layer()) + .service(tower::service_fn( + |_: axum::http::Request| async { + async {} + .instrument(tracing::info_span!( + target: "buzz_datastore", + "SELECT", + otel.kind = "client", + db.system.name = "postgresql", + )) + .await; + Ok::<_, std::convert::Infallible>(axum::response::Response::new( + axum::body::Body::empty(), + )) + }, + )); + + service + .oneshot( + axum::http::Request::get("/") + .body(axum::body::Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + provider.force_flush().unwrap(); + let spans = exporter.get_finished_spans().unwrap(); + let http = spans + .iter() + .find(|span| span.name == "http.request") + .unwrap(); + let datastore = spans.iter().find(|span| span.name == "SELECT").unwrap(); + + assert_eq!( + datastore.span_context.trace_id(), + http.span_context.trace_id() + ); + assert_eq!(datastore.parent_span_id, http.span_context.span_id()); + } + async fn handler_receives_message_with_limit(limit: usize, size: usize) -> bool { let (received_tx, mut received_rx) = mpsc::unbounded_channel(); let app = Router::new().route( diff --git a/crates/buzz-relay/src/telemetry.rs b/crates/buzz-relay/src/telemetry.rs index 8dd305cd4..11c6d0351 100644 --- a/crates/buzz-relay/src/telemetry.rs +++ b/crates/buzz-relay/src/telemetry.rs @@ -25,6 +25,16 @@ use opentelemetry_otlp::ExporterBuildError; use opentelemetry_sdk::{resource::EnvResourceDetector, trace::SdkTracerProvider, Resource}; +use tracing_subscriber::EnvFilter; + +/// Build the filter for spans exported through OpenTelemetry. +/// +/// This is intentionally independent from `RUST_LOG`: changing stdout log +/// verbosity must not remove parent spans from exported traces. Set +/// `BUZZ_OTEL_FILTER` to override the default targets. +pub fn otel_env_filter(configured: Option<&str>) -> EnvFilter { + EnvFilter::new(configured.unwrap_or("buzz_relay=info,buzz_datastore=info")) +} /// Build the OTEL [`Resource`] used by the trace provider. /// diff --git a/desktop/package.json b/desktop/package.json index 6726bfdca..adac095a4 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "buzz", "private": true, - "version": "0.4.26", + "version": "0.5.0", "type": "module", "scripts": { "dev": "vite", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 074b8f739..66553ef59 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1010,7 +1010,7 @@ dependencies = [ [[package]] name = "buzz-desktop" -version = "0.4.26" +version = "0.5.0" dependencies = [ "anyhow", "arboard", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index d68954468..324218a49 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "buzz-desktop" -version = "0.4.26" +version = "0.5.0" description = "Buzz desktop app" authors = ["you"] edition = "2021" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 07b721634..7a480c4c1 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Buzz", - "version": "0.4.26", + "version": "0.5.0", "identifier": "xyz.block.buzz.app", "build": { "beforeDevCommand": {