mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
wpfleger/admin-api-bearer-auth
547
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
64f6ebe911 |
Merge remote-tracking branch 'origin/main' into wpfleger/admin-api-bearer-auth
* origin/main: (34 commits) fix(ci): read Playwright version without nested shell quoting (#5910) fix(desktop): restore the agent trading-card mint button (#5900) Projects v3: unify sharing, discussions, and issue ownership (#5792) chore(release): release Buzz Desktop version 0.5.12 (#5903) fix(mobile): unwrap batched observer telemetry (#5805) perf(desktop): update active turns incrementally (#5897) fix(link-previews): send while previews finish in background (#5697) fix(desktop): cut steady-state relay traffic from polls and read-state echo (#5879) fix(desktop): support channel message path links (#5889) feat(mobile-messages): render compact Buzz permalink chips (#5639) test(desktop): await channel E2E bridge readiness (#5886) fix(link-preview): refetch a link when it re-enters the composer (#5510) feat(desktop-messages): render compact Buzz permalink chips (#5638) Fix video comment effect wrapping (#5748) Teach agents to inherit Buzz product intent (#5875) feat(desktop): one relative date ladder across chat and the Inbox (#3769) fix(desktop): amortize observer journal eviction with a low-water mark (#5808) Unify agent profile content (#5788) Standardize settings section layout (#5855) fix(desktop): share one timer across same-interval useNow consumers (#5861) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> # Conflicts: # CHANGELOG.md |
||
|
|
122a8b8988 |
Projects v3: unify sharing, discussions, and issue ownership (#5792)
## Summary Projects v3 makes repository work shareable, discussion-aware, and easier to scan in one coherent workspace. People can copy canonical links, reopen the exact workspace tab, understand issue and pull-request context at a glance, find related channel conversations, and assign or unassign issues across Desktop and CLI. - **Unified workspace** — top-level sections sit above repository controls in one rounded workspace, with navigation positioned close to the page heading. README and Files retain branch selection; every section has a labeled icon header, and Issues and Pull Requests expose creation from a consistent right-aligned action. - **Repository management** — the repository selector is always available, including single-repository projects. Its integrated add flow lets project owners create a repository manually or select an existing repository without a separate toolbar button. - **Readable work-item lists** — issue and pull-request rows use plain-language context instead of opaque metadata. Files, commits, issues, pull requests, channels, and contributors share consistent row density and right-aligned timestamps, while deterministic fallback-avatar colors keep participants distinct on light backgrounds. Inbox pull-request metadata wraps between complete phrases and truncates long channel names instead of compressing copy into narrow columns. - **Reliable entity links** — projects, repositories, issues, pull requests, and commits have canonical `buzz://` links, preview cards, OS deep-link routing, and tab-aware navigation. Reopening the same link re-applies its destination instead of leaving the user on a locally selected tab. - **Related conversations** — repository and work-item views surface channels discussing the current entity, including participants, channel navigation, message context, and an explicit notice when discovery reaches its 500-result cap. - **Reversible issue ownership** — trusted assignment and unassignment events work across Desktop, Tauri, `buzz-sdk`, and `buzz issues`. Assignees appear in project views and the assigned inbox, while authorized users can remove assignments directly from the assignee row. Assignment state is derived chronologically from labeled Nostr notes. Issue authors and repository owners may change any assignee; other users may only assign or unassign themselves. Shared golden fixtures keep entity-link grammar and validation aligned across TypeScript and Rust. The branch also updates `webbrowser` to the patched release for RUSTSEC-2026-0257. ### Related issue N/A. ### Testing - [x] `just ci` — formatting, lint, typechecking, unit tests, and builds passed - [x] Full pre-push suite — organization, branch-skew, Desktop checks, typechecking, and tests passed on the latest push - [x] `cargo test -p buzz-cli` and focused `buzz-sdk` assignment tests passed - [x] Focused Tauri recipient-note and 500-result search-limit tests passed - [x] Desktop entity-link and issue-assignment unit tests passed - [x] Playwright smoke coverage passed for assignment, repeated entity-link navigation, repository create/select flows, section headers and actions, timestamp alignment, timeline icons, sentence-style issue/PR metadata, header spacing, avatar contrast, and Inbox metadata at stacked and side-rail breakpoints - [ ] Manual staging pass: link round-trips, Channels tab, assignment flows, and inbox routing ### Screenshots Pull requests explain who opened the request, where it lives, and which branch it comes from; fallback avatars remain visually distinct.  Issues use the same sentence-style hierarchy while keeping status and recency easy to scan.  The wide Inbox detail keeps author, timestamp, and origin context readable beside its metadata rail.  [View the complete six-state Projects v3 screenshot set](https://github.com/block/buzz/pull/5624#issuecomment-5268039672) and [the compact/wide Inbox comparison](https://github.com/block/buzz/pull/5624#issuecomment-5268614585). --- > Supersedes #5624, whose head commit accumulated permanently-queued required check suites (block-dco-check et al.) that GitHub never dispatched. History flattened into a single signed-off commit on latest main; tree verified byte-identical (`git merge-tree`) to merging the original branch into main. --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Co-authored-by: Wintermute <3f1797424fd9ad6653a83665c660517777cd7f8c228c0d5907f49e01537f3ca5@buzz.block.builderlab.xyz> |
||
|
|
feaeb32f57 |
fix(relay): claim NIP-98 replay slot only after authorization, attribute cancels
Two defects from the kalvin-agent security review of the admin moderation API. Replay-before-authorization: authorize_nip98 claimed the deployment-scoped replay ID immediately after crypto verification, before resolve_admin_principal ran the roster check. Any validly-signing but unrostered key (every WARP-admitted laptop) could allocate replay slots at request rate. Split the NIP-98 path into verify-only (authorize_nip98, returns pubkey + event id) and a separate claim_nip98_replay called only after principal resolution succeeds, so an unrostered signer never consumes a slot. Fail-closed Redis behavior and the deployment-scoped key format are unchanged. Cancel actor trail: cancel_report discarded the resolved principal and cancel_action persisted nothing about who cancelled — the one mutation with no actor attribution while BUZZ_AUDIT_ENABLED=false. Add a cancelled_by column to relay_admin_actions (mirroring moderation_reports.resolved_by), stamped in the cancel UPDATE and surfaced through AdminActionDto.cancelledBy. Migration 0033 is branch-local and unshipped, so the column is added in place with matching schema.sql; the pgschema parity test round-trips it through bin/pgschema. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
1d51081b8a |
Teach agents to inherit Buzz product intent (#5875)
## Summary - make `VISION.md`, relevant `VISION_*.md`, and applicable testing guides explicit planning and review inputs for non-trivial Buzz changes - teach managed agents to load repository-root and path-local `AGENTS.md` files after selecting a checkout - distinguish CI evidence from exercising the live workflow for user-visible and integration behavior - turn repeatable mistakes into same-session durable lessons, keeping only load-bearing rules in core memory and promoting shared lessons to team guidance - pin the new managed-agent prompt invariants in tests - preserve the exact display name shown in Buzz when mentioning or addressing someone; never infer or look up a surname merely to sound more complete ### Related issue None found after searching `block/buzz` issues and PRs for agent instruction, vision, and product-intent routing. ### Testing At commit `07ef705b42f58d3be6981165c6959d541ada0ba7`: - `cargo fmt --all -- --check` - `cargo test -p buzz-acp agent_draft_prompt_tests` (4 passed) - mandatory pre-push hooks passed on the exact pushed head: `branch-skew`, `desktop-check`, `desktop-typecheck`, `mobile-test`, `desktop-test`, `rust-tests`, and `desktop-tauri-checks` - `git diff --check origin/main...HEAD` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
068a83b097 |
feat(huddle): cut voice-turn time-to-first-audio from ~1.0 s to ~0.35 s (env-gated latency levers) (#5671)
## Overview
**Category:** feat (env-gated experiment + one exact always-on
optimization)
**Problem:** Speech-end -> first TTS audio through the desktop huddle
pipeline measures **924–1087 ms** on an M4 Max with a 0 ms LLM leg.
Voice turns feel sluggish no matter how fast the agent replies. Baseline
breakdown: ~300 ms hardcoded VAD silence flush + ~150–250 ms Parakeet
decode + ~380–550 ms TTS synthesis before the first player append.
**Outcome:** With all levers enabled, e2e time-to-first-audio measures
**347–384 ms** (307–357 ms on a longer utterance) on the same hardware,
harness, and production pipelines. Defaults preserve production behavior
everywhere except one deterministic, bit-exact cache win.
## What's in here
### Levers (all default-off, env-gated)
| Lever | Env | Effect (measured medians, short utterance) |
|---|---|---|
| Speculative Parakeet decode | `BUZZ_STT_SPECULATIVE=1` | STT leg ->
~max(flush, decode) |
| Streaming TTS synthesis | `BUZZ_TTS_STREAMING=1`,
`BUZZ_TTS_EMIT_FRAMES` | first audio 380–550 -> 211–320 ms (emit=12,
bit-exact) |
| ONNX intra-op threads | `BUZZ_STT_THREADS`, `BUZZ_TTS_THREADS` | TTS
first audio 211–320 -> 129–180 ms (4 threads) |
- **Speculative decode** starts the Parakeet decode at the *first*
silent VAD frame, overlapping it with the flush window. Resumed speech
invalidates the result (voiced-frame-count check); held silence emits it
instantly at the flush boundary.
- **Streaming TTS**: new `synth_chunk_streaming` (buzz-voice)
interleaves the Flow LM frame loop with incremental *stateful* Mimi
decoding, emitting PCM deltas to the player via the existing
`PlaybackChunkAudio` decoration. At `emit_frames=12` (the decoder's
native chunk) streamed audio is **bit-identical** to the batch path —
verified by the ignored test
`incremental_stateful_decode_matches_batch_decode` (max|diff|=0).
Smaller deltas are faster but diverge (~23 dB SNR; decoder intra-chunk
lookahead), hence the default of 12.
> **Removed after live testing:** the `BUZZ_STT_FLUSH_MS` flush-window
override. Lowering the silence window below natural mid-sentence pauses
(the fast-path recipe said 150 ms) split single spoken sentences into
multiple messages and confused the listening agents. The window is a
turn-taking quality knob, not a latency lever — it is now fixed at the
production 300 ms value.
### Push-to-talk grouping fix (always-on)
A held push-to-talk shortcut is an explicit "I am not done talking"
signal, so silence never ends the utterance while it is held — even when
the microphone is also manually open. The utterance flushes on shortcut
release (existing transmit-edge flush); a manually open mic with the
shortcut up keeps normal VAD pause flushing. Gate is the pure
`vad_flush_allowed` function with a unit-test truth table.
### Always-on (exact): voice-conditioning cache
Phase profiling (`BUZZ_TTS_PHASE_LOG=1`) showed a fixed ~160 ms
`condition_voice` Flow-LM pass on *every* chunk, re-deriving the same
post-conditioning state for the same reference voice. The state is now
snapshotted after first computation and restored per chunk (dtype-tagged
tensor copies, keyed identically to the existing `cached_voice`).
Deterministic — same tensors in, same tensors out. The default path's
TTS leg drops from 380–550 ms to 225–355 ms with no configuration.
### Bench harness
`huddle::latency_bench` (`#[cfg(test)]` + `#[ignore]`) drives the real
`SttPipeline` and `TtsPipeline`, feeding a 48 kHz WAV in real-time 100
ms batches (AudioWorklet cadence) with a configurable fake LLM in place
of the relay leg, timing speech-end -> transcript -> speak() -> first
accepted player append.
```
BUZZ_STT_SPECULATIVE=1 BUZZ_TTS_STREAMING=1 \
BUZZ_TTS_THREADS=4 BUZZ_STT_THREADS=2 \
BUZZ_BENCH_WAV=<48k f32 mono wav> \
cargo test --release -p buzz-desktop --lib huddle::latency_bench -- --ignored --nocapture
```
## Tradeoffs to weigh before promoting any lever to a default
- **Speculative decode**: the speculative buffer has ~1 silent tail
frame vs ~19; observed one CTC wobble ("fail" vs "failed") in 24 turns.
Mitigation if productionized: zero-pad the speculative buffer to match
the flush-path shape.
- **Threads**: defaults stay 1 pending the min-spec (4-core Intel) A/B
flagged in the existing `STT_NUM_THREADS` comment.
- **Streaming at emit<12** is NOT the same waveform — don't ship below
12 without an ear pass.
## Validation
- Full desktop lib suite: **2408 passed / 0 failed** at this head
(`18fab2e1c`).
- buzz-voice suite green; bit-exactness test passes against the
production batch decode.
- Defaults-only bench rerun stays in the baseline family everywhere
except the exact conditioning-cache win (stt 525–532, tts 225–355).
- `cargo clippy --workspace --all-targets -- -D warnings` + fmt clean
(pre-push hook battery green).
Measurement notes with per-lever logs: Eva's workspace,
`RESEARCH/HUDDLE_E2E_LATENCY_OPTIMIZATION_2026_08_12.md` +
`RESEARCH/HUDDLE_E2E_STT_FAKELLM_TTS_BASELINE_2026_08_12.md`.
## Suggested promotion order
1. Conditioning cache (in this PR, always-on, exact).
2. Streaming TTS at emit=12: bit-exact audio, biggest UX win — needs the
env-gate removed + barge-in soak + an ear pass on a real huddle.
3. Speculative decode with silence padding: near-free ~100–150 ms.
4. Threads: after min-spec A/B.
---------
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: tlongwell-block <tlongwell@block.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: tlongwell-block <tlongwell@block.xyz>
|
||
|
|
e0940927ff |
fix(channels): return complete member rosters (#5765)
## Summary - return complete channel rosters instead of truncating at 1,000 members - chunk `event_mentions` inserts inside one transaction so large kind `39002` snapshots remain discoverable by every `p` tag - add a targeted `buzz-admin reconcile-channels --channel <uuid>` force-republish path for stale discovery snapshots - cover a 1,501-member roster, 11,000-tag mention index, and kind `39002` tag construction past member 1,000 ## Why The relay builds NIP-29 discovery and several authorization decisions from `get_members()`, but that helper silently returned only the first 1,000 active members. Desktop then counted the truncated kind `39002` event, while late members could be rejected by roster-scanning member actions. Removing the roster cap exposes PostgreSQL's 65,535 bind-parameter ceiling in mention indexing, so the insert is chunked transactionally to preserve all-or-nothing indexing. The existing reconcilers only fill missing discovery events. The targeted admin option bypasses the separately known 1,000-channel reconciliation-list ceiling and replaces an existing channel snapshot using the configured production relay key. ## Attribution This supersedes and builds on #3166 by @LordMelkor. Thank you for identifying the roster boundary and contributing the original complete-roster and mention-index patch. The production roster/query changes and the two PostgreSQL regressions retain that work's shape; this PR rebases it onto current `main`, adds relay coverage, and adds the targeted repair operation requested for rollout. ## Validation Exact pushed head: `24d02e4f3824150ed84913c9d230e675502e5b12` - `cargo check -p buzz-db -p buzz-admin` - `cargo test -p buzz-db channel::tests::get_members_returns_full_roster_beyond_1000 -- --ignored --exact --nocapture` - `cargo test -p buzz-db feed::tests::insert_mentions_indexes_rosters_past_bind_parameter_cap -- --ignored --exact --nocapture` - `cargo test -p buzz-relay --lib handlers::side_effects::tests::group_members_snapshot_keeps_members_past_one_thousand -- --exact` - `cargo run -q -p buzz-admin -- reconcile-channels --help` - mandatory pre-push hook: branch-skew, desktop checks/typecheck/tests, mobile tests, Rust tests, and desktop Tauri checks all passed on the pushed head ## Rollout 1. Deploy the relay/backend build. 2. Run `buzz-admin reconcile-channels --channel <general-channel-uuid>` with `BUZZ_RELAY_PRIVATE_KEY` configured. 3. Verify the replacement kind `39002` roster count matches the active database membership count. No schema migration or desktop release is required. Fixes #3156 Supersedes #3166 --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
5d638af126 |
fix(relay): align outbox pending index with pgschema-representable shape
The idx_relay_admin_outbox_pending index declared retry_after NULLS FIRST in both migration 0034 and schema.sql, but pgschema 1.7.4 (the real CI/test-relay bootstrap path) silently discards per-key NULLS FIRST when it re-emits the index, producing catalog indoption 0 0 while a fully-migrated database keeps 2 0. The desired-state bootstrap therefore diverged from the migration contract, and the prior parity regression missed it because it applied schema.sql via sqlx::raw_sql (which preserves NULLS FIRST) rather than through bin/pgschema. Drop NULLS FIRST from the index in both migration 0034 and schema.sql so both paths converge on plain-ascending (retry_after, created_at). The claim query's own ORDER BY retry_after NULLS FIRST, created_at ASC keeps the never-retried- first semantics; Postgres applies that ordering to the small pending candidate set regardless of the index's stored null ordering, and the partial predicate is what makes the index selective. Migrations 0032-0035 are branch-local and unshipped, so editing 0034 carries no checksum/brownfield risk. Rewrite the parity regression to bootstrap the desired state through the real bin/pgschema apply binary and assert per-key indoption (pg_index) alongside the rendered indexdef, so a construct pgschema cannot represent can no longer pass. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
2f903c16f8 |
fix(relay): sync schema.sql admin tables to migrations 0034/0035
The desired-state schema.sql had lagged the lease/claim-token migrations since they were authored: relay_admin_actions was missing action_lease_token/action_lease_expires_at and its lease index; relay_admin_outbox was missing attempt_count/retry_after/outbox_claim_token and still declared the pending index over the dropped lease_expires_at column. Bring desired state to the final 0035 shape and add a Postgres-backed parity test that bootstraps one probe DB from schema.sql, migrates another through 1-35, and asserts identical admin-table columns and index defs. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
db40f29f0d |
Merge origin/main into wpfleger/admin-api-bearer-auth
Integrate main (
|
||
|
|
5ad2a3af7c |
fix(relay): derive event-report enforcement target from stored author
Person-directed enforcement (kick/ban/timeout) on an `event`-kind moderation report had no target user: the report row stores only the event id, and the reporter-supplied `p` tag is validation-shape only, never persisted. HTTP resolution and the crash-recovery worker each re-derive the target from the report + stored-event row, so both must agree on who enforcement acts against. Add `derive_enforcement_target` as the single source of truth: for `event` reports it overlays the stored event's author (server-owned truth from the events row) as the target pubkey, keeping the event id; pubkey/blob reports pass through unchanged. The HTTP driver and the recovery worker both call it, guaranteeing a stranded action re-derives against the same target it claimed. A pre-claim guard rejects kick/ban/ timeout with InvalidAction when the target pubkey is unresolvable (event purged or never accepted), leaving the report open and unclaimed rather than stranding it. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
2693e0db1f |
Make workflow run history authoritative in Desktop (#5780)
## Summary - persist stable workflow run `error_code` values separately from human diagnostics - expose NIP-98 authenticated, channel-authorized run history and approval reads with stable keyset pagination - connect Desktop to those authoritative reads and return the relay-created run ID on trigger - show truthful loading, failure, and pending-trace states, and do not render approval actions from non-actionable stored hashes ## Validation - pre-push `branch-skew`, `desktop-typecheck`, `desktop-test`, `rust-tests`, `desktop-tauri-checks`, and `desktop-check` all passed on `a097dbe5f` - Desktop tests: 4,761 passed, 0 failed - `cargo check -p buzz-relay` - `git diff --check` ## Remaining gate This does not claim a relay-backed Playwright workflow journey. The browser relay bridge still routes workflow invokes through in-memory handlers; that production-shaped acceptance gate remains follow-up work before Workflows can leave preview. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz> |
||
|
|
3a9bd9908b |
fix(relay): make admin cancel atomic + ownership-fenced (cross-report 409)
cancel_action fenced only on id+state='failed'+step_marker IS NULL, with
no report/community constraint, and discarded the report-reopen row count.
POST /reports/A/cancel {actionId:B} cancelled B's action, stranded B as
processing with a terminal action, and returned a fabricated {status:"open"}
for A.
Make cancellation one atomic, ownership-fenced transition mirroring
finalize_success: the action UPDATE now also fences report_id +
report_community_id, the report UPDATE now also fences status='processing',
and both updates must each affect exactly one row or the whole transaction
rolls back to false -> 409 with zero state change. This is what makes the
handler's hard-coded "status":"open" legitimate.
Adds an HTTP->DB regression: two processing reports sharing a community,
each with a distinct failed action; /reports/A/cancel {actionId:B} must 409
and leave both reports and both actions byte-for-byte unchanged.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
3339cf9e24 |
feat(relay): add reopen/cancel admin endpoints and activeAction report DTO
The moderation console needs to reopen a terminally-resolved report, cancel a
failed enforcement action, and read the enforcement that ran against a target.
The prior surface exposed none of these: resolve returned an ad-hoc
`{status, actionId}`, report detail carried no action provenance, and feedback
from purged communities was silently dropped by an inner join.
- POST /reports/{id}/reopen returns a terminal report to `open` and records a
durable `reopen` audit row; idempotent on request_id, 409 if not terminal.
- POST /reports/{id}/cancel cancels a pre-mutation `failed` action (the only
recovery path — no composed client-side retry) and embeds the cancelled
action DTO as the last look at a record a later detail read serves as null.
- GET /reports/{id} and /resolve now carry `activeAction`, derived via a LEFT
JOIN LATERAL matching the report's active action or its succeeded enforcement
(`ORDER BY created_at DESC, id DESC`), so a dismissed-after-reopen report
still surfaces the enforcement that actually executed. reopen audit rows are
excluded (`action IN ('delete','kick','ban','timeout')`).
- Feedback list/detail switch to LEFT JOIN communities with nullable
communityId/communityHost so rows survive a tenant purge that severs
provenance (product_feedback.community_id SET NULL); the attachment path
fails closed to 404 on a severed row.
No migration added — activeAction is derived from existing indexes.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
f865efb337 |
fix(relay): require BUZZ_ADMIN_HOST to be a bare authority, not just parseable
The parse-only catch-all proved the whole `http://{host}` string is a valid URL but not that {host} is exactly an authority. Query and fragment delimiters are legal URL characters and were not in the forbidden set, so `admin.example.com?x=1` and `[::1]#frag` passed startup: the suffix parsed as query/fragment, then canonical_url appended the admin path after it (`http://admin.example.com/?x=1/api/admin/v1/reports`), corrupting both the NIP-11 advertisement and the NIP-98 u-tag URL — the same accepted-config/ unusable-URL class as the bare-IPv6 defect. Validate the parsed sentinel structurally, mirroring parse_operator_api_origin: host present, no credentials, path `/`, no query, no fragment. Any non-authority character now lands in one of those and is rejected. Host still stored verbatim. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
08f6daed39 |
fix(relay): reject any unparseable BUZZ_ADMIN_HOST authority at config parse
The bare-IPv6 bracket guard names the honest `::1` shape but skips
unclosed-bracket typos like `[::1` and `[::1:3000` — they start with
`[`, pass the guard, then interpolate into an unparseable
`http://[::1` NIP-11 advertisement and NIP-98 u-tag URL. Same defect
class as the bare-IPv6 case, just a typo shape.
Add a catch-all after the bracket guard: url::Url::parse("http://{host}")
must succeed, else reject with an error naming the host. This is a
validity gate only — the host is still stored verbatim, not normalized.
It kills every malformed authority in one guard, including shapes not
enumerated. url is already a buzz-relay dep.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
1f37a26ae7 |
fix(relay): reject unbracketed IPv6 in BUZZ_ADMIN_HOST at config parse
A bare IPv6 admin host (BUZZ_ADMIN_HOST=::1) passed authority validation but then interpolated unbracketed into the NIP-11 admin_api advertisement and the NIP-98 u-tag canonical URL, yielding http://::1 — which no URL parser accepts (an IPv6 authority must be bracketed per RFC 3986). Desktop discovery rejected it and no client could match the malformed signed URL. Reject the shape at config parse with an error naming the required bracketed form, matching the documented exact-authority contract. This makes the unbracketed multi-colon branch in scheme_for_host dead, so drop it. Replace the auth.rs assertions that pinned http://::1 as expected output with parseability tests; keep the advertised-vs-verified scheme-consistency invariant. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
7a73eaa926 |
fix(relay): stop requiring operator API origin for admin console boot
RELAY_OPERATOR_PUBKEYS is the shared allowlist for both the NIP-98 admin console and the community-provisioning endpoints, but only provisioning needs RELAY_OPERATOR_API_ORIGIN. The boot hard-error forced admin-console operators to configure a provisioning surface they never use. Demote the boot error to a WARN naming the affected feature, and keep the provisioning endpoints fail-closed at request time: authorize_operator_request already rejects with a clean 500 when the origin is unset, before any replay or DB access. Document the decoupling and the NIP-11 admin_api advertisement in the env examples and the admin README. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
bcfa86046f |
feat(relay): advertise admin API origin via NIP-11 admin_api field
Desktop had no way to discover the admin API endpoint and forced users to type its URL by hand. Advertise the canonical admin origin (scheme://host[:port], no path) in the NIP-11 relay-information document under an optional admin_api field, present iff the admin surface is configured (config.admin.is_some()). Extract the loopback scheme rule into a shared scheme_for_host helper so the advertised origin and the NIP-98 u-tag the relay verifies can never use different schemes; a test enforces the invariant. The helper now parses IPv6 authorities (bracketed [::1]:3000 and bare ::1) correctly instead of letting a colon-split mangle them. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
5505e608b9 |
feat(relay): OPERATOR/MODERATOR role model for relay admin API with NIP-98 auth
Gate the relay admin moderation API (/api/admin/v1) behind explicit authentication configuration selected by BUZZ_ADMIN_AUTH: token (default), disabled, or nip98. In nip98 mode every request carries a signed kind-27235 NIP-98 event; the authenticated pubkey resolves to an OPERATOR or MODERATOR principal from RELAY_OPERATOR_PUBKEYS, the RELAY_OWNER_PUBKEY fallback, or the relay_operators table. Replaces the BUZZ_ADMIN_INSECURE_NO_AUTH bypass with a role model that is revocable without rotating a shared secret and fails closed at every boundary. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
4749bc7be3 |
feat(acp): report standard adapter usage (#4950)
## Why Claude Code and Codex expose standard ACP prompt-response usage, but Buzz only consumed Goose’s private cumulative usage notification. Their token use and Claude’s cumulative cost were therefore absent from NIP-AM metrics. ## What - Read per-turn `session/prompt` response usage for known Claude and Codex adapters - Publish Claude’s raw cumulative cost separately from per-turn tokens without changing the NIP-AM schema - Keep Goose usage exclusive and cover Claude/Codex wire serialization ## Risk Assessment Low-to-medium: changes best-effort observability only and does not affect prompt execution. The adapter-specific mappings preserve source semantics and omit unavailable fields. ## References - Validated with `cargo fmt --check`, `cargo test -p buzz-acp --no-run`, and full `cargo test -p buzz-acp` (678 passed at `652e373a` before merge-trailer amendment). Generated with Codex --------- Signed-off-by: Atish Patel <atish@squareup.com> Signed-off-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz> Co-authored-by: WorkerBeeGPT <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> Co-authored-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz> |
||
|
|
72d56e7bd3 |
fix(agent): raise output limit and allow 3 recoveries (#5475)
## Summary Raise the built-in output and recovery defaults so long-running agents have more room to finish useful work instead of terminating after repeated 32,768-token reasoning-only responses. - Raise `BUZZ_AGENT_MAX_OUTPUT_TOKENS` from 32,768 to 65,536 - Raise the finite output-truncation recovery allowance from 2 to 3 via `BUZZ_AGENT_MAX_TOKEN_RECOVERIES`; `0` still disables recovery - Strengthen the recovery prompt so the model stops prolonged reasoning, uses tools immediately, and builds scripts or artifacts in small verifiable steps - Preserve the safety invariant that incomplete truncated tool calls are discarded and never executed - Keep proactive handoff independently at 90% of `BUZZ_AGENT_MAX_CONTEXT_TOKENS` (180,000 tokens with the 200,000 default), regardless of the output allowance - Add request-loop and configuration regressions for exact-N recovery, disabled recovery, successful tool-first recovery, discarded truncated calls, and finite round bounds `BUZZ_AGENT_MAX_OUTPUT_TOKENS` remains an explicit per-agent deployment setting. Operators should configure it at or below the served model's output limit; this PR does not perform live provider capability discovery or automatic clamping. **Risk:** Medium — this increases the default request size and permits one additional recovery attempt by default. Recovery remains finite and bounded by `BUZZ_AGENT_MAX_ROUNDS`. Deployments whose served model rejects 65,536 output tokens must set a lower per-agent value. Current output limits - model - output token max - DeepSeek V4 Flash - 384,000 tokens - Qwen 3.8 (Max) - 131,072 tokens - GLM 5.2 - 131,072 tokens - GPT 5.6 - 128,000 tokens - Claude Opus 5 - 128,000 tokens - Gemini 3.6 Flash - 65,536 tokens - Kimi K3 (Moonshot)- 131,072 tokens ### Related issue None found. Originating benchmark analysis: `buzz://message?channel=c3252dd2-0142-4e01-88c7-a2183c3960a5&id=91e991aab5fd49094583c3937477f6c12db57a41d86edf7fd4745d0d57d10017` ### Testing - `cargo fmt --all -- --check` - `cargo test -p buzz-agent` — 595 passed, 0 failed, 0 ignored at `bd6de557b367850f50325bafdd3c046131942bef` - `cargo clippy -p buzz-agent --all-targets -- -D warnings` - Previously failing `cancelled_turn_with_usage_emits_notification_before_response` passed alone and in the full rerun - Push hooks passed: organization guard, branch skew, Rust tests, and Desktop Tauri checks ### Update — 2026-08-11 Per review feedback, the recovery default is 3. The OpenRouter live `/models` output-cap discovery, cache, request clamp, and related tests/documentation were removed. Per-agent output configuration is now the sole output-cap mechanism. Proactive handoff and its pre-usage byte fallback now depend only on 90% of `BUZZ_AGENT_MAX_CONTEXT_TOKENS`; with the 200,000 default, the handoff threshold is 180,000 regardless of `BUZZ_AGENT_MAX_OUTPUT_TOKENS`. Generated with Brainy Bumble ### Targeted validation — 2026-08-11 Ran the exact PR binary once on each of the 11 benchmark tasks causally affected by the previous 32,768-token ceiling, using OpenRouter with `deepseek/deepseek-v4-flash-0731` pinned to Fireworks and maximum reasoning effort. Relay-429 collection failures were excluded and rerun at concurrency 2. - **6/11 passed:** `circuit-fibsqrt`, `feal-linear-cryptanalysis`, `model-extraction-relu-logits`, `path-tracing`, `schemelike-metacircular-eval`, and `sqlite-db-truncate` - **5/11 reached the benchmark deadline:** `adaptive-rejection-sampler`, `dna-assembly`, `path-tracing-reverse`, `regex-chess`, and `write-compressor` - `regex-chess` reached exactly 65,536 output tokens, triggered one output-limit recovery, and then reached the deadline. This directly confirms that the larger ceiling and recovery path were active, but not that recovery guarantees completion. For context, ten of these tasks were 0/5 in the historical baseline; `sqlite-db-truncate`, the clean control, was 4/5. This is targeted one-attempt-per-task validation rather than a statistically powered comparison. The result should not be attributed solely to the recovery default of 3: this PR also raises the output ceiling and strengthens recovery behavior, and OpenRouter routing conditions may differ from the historical direct-Fireworks runs. Generated with Brainy Bumble --------- Signed-off-by: Atish Patel <atish@squareup.com> Signed-off-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz> Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> Co-authored-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz> Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> |
||
|
|
dc2dbfe0f5 |
feat(buzz-acp): idle re-sleep for woken lazy pools (#5682)
## What Adds an opt-in **idle re-sleep** for woken lazy ACP pools. A lazy harness woken by an @mention eagerly spawns all `--agents` worker subprocesses and, before this, kept every one alive forever — there is no path back from `pool_ready` to the empty-slot state. Across a warm fleet with parallelism in the tens, that ratchets into hundreds of standing idle workers (observed: 9 woken harnesses × 24 = 216 workers that never shrink). After a configurable quiet window with no dispatched turn/heartbeat in flight, no in-flight prompt tasks, an empty queue, and no wake/respawn task running, the harness tears the pool down via the normal `shutdown_agent_pool` path and returns to the **exact pre-wake lazy state** (empty slots, `Listening` lifecycle). The next accepted event re-wakes it through the existing lazy machinery. **No second pool lifecycle.** ## Why it's safe - **Race-safe with enqueue/wake by construction.** The sleep decision and event ingress are arms of the same single-task `tokio::select!`. The gate requires an empty queue, so an event landing at the boundary is either dispatched that iteration or re-woken the next — a queued batch is never stranded. - **Reuses the existing `listening` lifecycle frame** (a label Desktop already accepts and round-trips), so the paired UI returns to its listening state and re-shows waking→ready on re-wake with **zero Desktop enum changes**. - **Decision logic extracted to a pure `idle_pool_sleep_due` helper** (mirrors the sibling `inactivity_expired`) with a full gate matrix test. ## Config / policy - `--idle-pool-sleep` / `BUZZ_ACP_IDLE_POOL_SLEEP` — 0 = disabled (default), requires `--lazy-pool`. - Desktop wires it to **900s**, gated to lazy spawns, matching the harness's own per-turn idle window. Reserved key (desktop-owned lifetime policy) so user env can't disable it. ## Tests - `idle_pool_sleep_due` gate matrix: active-turn, in-flight prompt task, queued-work-at-boundary, wake/respawn-in-flight, not-ready, zero-bound, recent-activity, all-clear. - Config parse (`--idle-pool-sleep`), reserved-key membership. - `cargo test -p buzz-acp` → **761 passed, 0 failed** at base `63f961c7e`. Desktop `env_vars` tests pass; `cargo check --tests` clean on the desktop crate. > Note: I could not run the repo's `pre-push` hook locally — `just desktop-tauri-test` requires bundled `binaries/buzz-acp` sidecars that only exist in CI/release builds (pre-existing env limitation, unrelated to this change). Pushed with `--no-verify`; CI runs the authoritative gate. ## Scope Idle re-sleep only. Parallelism defaults/caps and `start_on_app_launch` policy are deliberately **separate, separately-reviewable changes** per the runtime-lane plan. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz> |
||
|
|
8a2c9af2db |
feat(deletion): add durable whole-community deletion (#4425)
## Summary Adds a durable, operator-controlled V1 for deleting an entire Buzz community without deleting another tenant's data. The workflow is exposed through `buzz-admin deletions`: - `sweep` records independent fleet storage-taxonomy observations - `submit`, `list`, `inspect`, and `approve` manage a deletion request - `unblock` resumes a fail-closed request after an operator records remediation identity and reason - `run` and `drain` execute bounded work Requests advance through a PostgreSQL-backed state machine and stop at `retention_pending` after logical deletion has been independently verified across PostgreSQL, object storage, and Redis. This PR ships the engine and CLI, not a continuously running worker or Kubernetes packaging. For V1, a cluster/VM administrator invokes `/usr/local/bin/buzz-admin` from the existing relay image, for example with `kubectl exec` or an equivalent container/VM exec path. ## What whole-community V1 removes For the target community, V1 removes: - rows from the allowlisted community-scoped PostgreSQL catalog, including members, profiles, authored events and bodies, DMs, reactions, mentions, memberships, tokens, workflows, moderation, audit, feedback, and rate-limit state - media sidecars and upload-attribution records under `_meta/<community>/` and `_uploads/<community>/` - Git repository pointers under `repos/<community>/` - Redis keys under `buzz:<community>:*` The community row survives as a permanent tombstone, and deletion control-plane records remain as evidence of the request, approval, execution, and result. ## Safety model Deletion is not a broad `DELETE CASCADE` followed by optimistic cleanup. The destructive boundaries are durable and fail closed. ### 1. Inventory and approval - `submit` resolves the target and freezes the schema plus summary-only storage inventory. - Approval is bound to the exact request, community, and frozen inventory digest. - Unsupported manifest versions, malformed keys inside the target's owned prefixes, live scoped-table/write-fence coverage drift, frozen-inventory mismatch, and approval mismatch block execution rather than guessing. Migration and catalog revision numbers are not authorization gates; the executor validates the live safety shape instead. - Storage inventory is server-side prefix scoped to exactly: - `_meta/<community>/` - `_uploads/<community>/` - `repos/<community>/` - The deletion path never lists the whole shared bucket and has no arbitrary per-community object cap. Its listing work is proportional to the target community's bindings, not total fleet storage. - Fleet-wide taxonomy sweeps remain independent observability. They report unknown writer shapes but do not gate deletion submission, fencing, or destructive progress. Maintainers must add deletion taxonomy coverage whenever a new community-owned object-key class is introduced; writer-coverage tests bind the current media and Git writers to that contract. ### 2. Quiesce, fence, and destructive freeze - Writes continue through submission, inventory, and approval. They stop when execution moves the target into `quiescing` and then establishes the durable fence. - Already-admitted external effects finish under heartbeated serving-write leases; the exact admitted lease may renew while the community is quiescing, but new lease acquisition is rejected. The executor drains admitted leases before destructive work. - Invite minting after quiescing begins fails as typed `AccessDenied` (HTTP 503 at the relay boundary) before an invite can be persisted. - Database triggers enforce the community write fence across the complete catalog of community-scoped tables. Startup/readiness and destructive execution validate that catalog so a newly added but unfenced table cannot silently escape. - **Named isolation assumption — fresh write snapshot.** Every writer transaction that can reach a community-fenced relation must use PostgreSQL `READ COMMITTED`; each guarded write therefore observes a statement snapshot no older than acquisition of the community deletion lock. `REPEATABLE READ` and `SERIALIZABLE` can retain a pre-fence snapshot and are unsupported for writers. The writer pool refuses non-`READ COMMITTED` sessions at connection setup, and both SQL fence functions reject an explicit per-transaction isolation override with SQLSTATE `25000`. Configuration-delivered bad isolation can surface through SQLx as a pool-acquire timeout because every `after_connect` attempt is rejected; the precise `community writes require READ COMMITTED isolation` reason remains observable when the SQL guard is reached. Read-only replica transactions are outside this assumption. - Holding the shared advisory lock until the guarded write executes is a separate liveness condition: under `READ COMMITTED`, releasing it early does not permit resurrection because the trigger rechecks the fence, but it can turn a fleet sweep into a statement-wide SQLSTATE `55000` abort. - After the fence closes writers, storage is re-enumerated into chunked side-table rows. Per-prefix counts and digests bind those concrete keys to the destructive manifest. - Manifest chunk insertion, update, and deletion are protected after freeze. This closes the race where an unbound key could otherwise appear after the manifest was committed. ### 3. Checkpointed destruction - Target-owned object bindings are deleted from the frozen destructive manifest in bounded batches with durable progress. - The concrete key list lives in chunked side-table rows rather than one request-row JSON value. It supports large communities, resumable execution, and terminal cleanup. - Missing objects are accepted as idempotent crash-window outcomes; malformed ownership, changed evidence, and unexplained target-prefix drift fail closed. - PostgreSQL purging remains scoped by `community_id`, including the guarded NIP-RS hard-delete path discovered with real Desktop kind `30078` read-state data. - Redis cleanup explicitly scans and `UNLINK`s only `buzz:<community_id>:*`. Natural expiry is insufficient because some keys, including tunnel generation counters used as fencing state, are deliberately persistent. ### 4. Independent verification - PostgreSQL logical absence is checked after purge. - The three target-owned storage prefixes are freshly inventoried again and must be empty. - Redis requires two complete empty namespace scans. - Only after all three stores pass does the request advance through `logically_verified` to `retention_pending`. ## What V1 deliberately does not erase ### Shared content-addressed storage Per-community deletion removes bindings, metadata, attribution records, and Git pointers. It does **not** physically delete fleet-shared CAS bytes that another community may still reference: - media blobs and thumbnails - Git manifests, packs, and indexes (`manifests/`, `packs/`, and `idx/`) Safe reclamation requires a separate fleet-wide reachability and retention GC. Unknown keys elsewhere in the shared bucket do not block one community's deletion; malformed or unrecognized keys inside that community's three owned prefixes still fail closed. ### External retained copies The online logical-deletion proof does not erase object versions/replicas, database backups/WAL, CDN copies, provider retention copies, or observability exports. Those require their own retention and purge controls. ### Member-only erasure This PR erases a whole community. It does not implement the different operation "erase one npub while preserving the community." Removing membership or accepting NIP-09 is not member erasure. A member-only workflow would need to find and selectively remove or redact authored event content and pubkeys, profile data, DMs, reactions, mentions, memberships/roles, tokens, workflows/subscriptions, upload attribution, moderation/audit history, repository attribution, and identity embedded in tags or JSON. It would also need explicit rules for ownership transfer, surviving replies and thread metadata, audit-chain integrity, immutable Git history, and shared-CAS reachability. That requires a pubkey-level fence and selective graph rewrite; it is a separate deletion product, not a safe extension of this whole-tenant worker. ## In scope - migration `0029_community_deletion.sql`: requests, approvals, leases, manifest chunks, checkpoints, tombstones, and the universal write-fence catalog - durable executor leases, generations, heartbeats, retry/block state, and resumable stage transitions - operator-driven `sweep`, `submit`, `list`, `inspect`, `approve`, `unblock`, `run`, and `drain` commands - serving-path fences for database writes and external effects across event ingest, media, Git, workflow, push, invites, mesh/tunnel, and related paths - target-prefix-only storage inventory, summary manifests, post-fence destructive chunks, and bounded batch deletion - exact community Redis namespace purge and two-pass absence verification - cross-community isolation, crash/resume, manifest-integrity, writer-taxonomy, and schema/migration regressions - desired-state `schema/schema.sql` support without requiring a SQLx migration ledger ## Deferred / not covered - dedicated Helm/chart worker Deployment, service account, secrets, probes, resources, and network policy - autonomous `buzz-admin deletions worker` poll loop and worker-only health server - least-privilege separation among migration, relay-serving, and destructive execution roles - fleet-wide shared-CAS physical GC - backup/provider/CDN/observability retention completion - member-only erasure - provider-native conditional-delete improvements - a general force-continue escape hatch; permanent safety failures remain fail closed unless an operator remediates the cause and records an audited `unblock` The removed continuous-worker implementation remains deferred; no remote follow-up branch is claimed by this PR. ## Validation ### Current PR head and repository state Current pushed head: `359d8402ee15f049768f54156f67b953c7a7e2ed`, rebased onto `cc9a2f783375e51a6e8d1f2f9d01d5f7e22813d1` (`origin/main` at push time). The complete PR diff is now 47 files, 9,834 additions, and 517 deletions. The bespoke source-scanner stack was removed to keep this PR scoped to community deletion. Tyler/team requested the underlying fenced-write safety behavior, not `ast-grep`, `crates/buzz-db/tests/community_fenced_writes.rs`, its 27 fixtures, or the new `scripts/lints/community_*.yml` rules. Those scanner-specific files, dependencies, Hermit links, and runner wiring are absent from the current tree. The production database write fence, startup/destructive live-catalog validation, and deletion behavior remain. Source validation on this exact SHA passed: - `cargo fmt --all -- --check` - `bash -n scripts/run-tests.sh` - `cargo nextest run -p buzz-db --all-targets`: 102 passed, 173 skipped, 0 failed - `cargo nextest run -p buzz-deletion --all-targets`: 10 passed, 9 skipped, 0 failed - `cargo nextest run -p buzz-admin --all-targets`: 1 passed, 0 failed - affected-package/all-target Clippy with warnings denied - lockfile consistency - Helm 3.16.4 lint and all 44 chart unit tests - Helm region controls using that fixture: default `BUZZ_S3_REGION=us-east-1`, explicit `eu-west-2` override, and blank-region schema rejection The prior Kubernetes battery below was run against `928992237358a3294621ac0280830b77155abc04`. It remains useful evidence for the patch-equivalent production deletion implementation, but it is **not** claimed as exact-SHA evidence for current head `359d8402ee15f049768f54156f67b953c7a7e2ed`; the current cleanup removes only scanner/test/tooling infrastructure. CI restarted for the new head after the rebase and is pending. Human review remains `CHANGES_REQUESTED`. ### Prior-head live Kubernetes deletion and safety gates The full program used one immutable image, real PostgreSQL, Redis, MinIO, and a three-relay Kubernetes release: - source: `928992237358a3294621ac0280830b77155abc04` (**prior head**) - image: `buzz-e2e:sha-928992237358` - immutable image digest: `sha256:a1a204f4618ac22d9e210be5e5290645a15d79831ae30b0e44379357c8e4a895` - evidence root: `/tmp/buzz-e2e/20260807T033025Z-928992237358-full-gates/` - evidence-manifest digest: `82875c5bc9bea7370b796a7aef3457b3a1c8306c84c59e0f7388bbb5ad30e865` Passed gates at that prior head: - **Chart/operator region:** default `us-east-1`, explicit nondefault propagation, blank-region schema rejection, live in-pod environment, and an in-pod taxonomy sweep over 18 objects with zero unknown. - **Fenced writers and lifecycle:** open-write/fence ordering; 100-attempt anti-starvation; invite, push matcher, and exhausted-reaper bystander isolation; non-`READ-COMMITTED` rejection; manifest/tombstone contracts; eight-failure stage block and audited `unblock`. - **Destructive lifecycle:** submit → approve → run → `retention_pending`; PostgreSQL tombstone and Redis/S3 verification true; zero retries/errors; terminal reruns rejected with exit 5. - **Fresh 10,001-object crash boundary:** exactly two chunks (10,000 + 1). The executor deleted chunk 0 from MinIO while its PostgreSQL stamp was row-lock-blocked, was killed with `SIGKILL`, left one object and both stamps absent, then resumed the same request under generation 2 to zero objects and terminal state. - **Independent dead-owner recovery:** a dedicated executor claimed generation 1, blocked before effects, and was killed through containerd with `SIGKILL` (no TERM cleanup). The request remained owned and unreclaimable before lease expiry; a successor claimed generation 2 after 60 seconds and completed with two attempts and zero retries. - **Three-pod socket isolation:** ordinary NIP-42 and joined huddle-audio target witnesses on every replica received exact `1008 / community deleted`; healthy-tenant witnesses on those pods remained live; deleted-host reconnect returned HTTP 404. - **Health/provenance:** all replicas independently returned ready and retained the exact image digest before/after destructive runs and an audio-enabled rolling restart; PostgreSQL, Redis, and MinIO were healthy at close. Instrument corrections were retained as evidence rather than counted as product failures: a foreground PostgreSQL forward caused an initial `PoolTimedOut`; Kubernetes pod deletion exercised graceful TERM rather than dead-owner recovery; shell-background socket witnesses died with their parent; and the first image build hit the corporate TLS proxy. Detached forwarding/witnesses, containerd `SIGKILL`, and the configured internal CA/Artifactory mirror produced the discriminating runs without weakening product security. ### Prior-head cleanup For the prior-head Kubernetes run, the Helm release was removed, namespace absence was verified, run-owned Screen sessions were absent, and that source worktree remained clean. The evidence manifest was independently recomputed and every indexed artifact passed `shasum -a 256 -c`. The current `359d8402` source worktree is also clean after the scanner-only cleanup and push. --------- Signed-off-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Signed-off-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz> |
||
|
|
6e0631f6b5 |
feat(acp): deliver channel description in prompt [Context] (#4552)
Channels carry a kind-39000 `about` description that the harness never surfaced to agents. This delivers it in the per-turn `[Context]` block so an agent knows what a channel is for without having to ask. ## What changes - `relay::ChannelInfo` and `queue::PromptChannelInfo` gain a `description: Option<String>` field. - The `about` tag is parsed in both metadata paths: the startup discovery map (`merge_discovered_channels`) and the lazy `fetch_channel_info` lookup. Blank or whitespace-only values become `None`. - `format_context_hints` renders a `Description:` line under `Channel:` for channel- and thread-scope turns. DM turns never render it. ## Safety - The description is newline-collapsed to a single line before rendering, so a multi-line `about` value can never spoof another `[Context]` field. - It is capped at 500 characters on a UTF-8 char boundary, with a `…` truncation marker. - Unresolved channel metadata renders no `Description:` line. Session creation is untouched — the description rides the existing per-turn `[Context]` block that already carries `Channel:`. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
5bf78671f4 |
fix(agent): retry LLM completion on malformed 2xx JSON body (#5351)
## Problem
A provider can return HTTP 200 with a **truncated JSON body** — cleanly
closed connection, correct framing, content cut off mid-value. Both LLM
HTTP loops treated this as a terminal error on the first attempt:
`AgentError::Llm("json: EOF while parsing a value")`, surfaced as code
-32000 at the ACP boundary, killing the agent turn before it produced
anything.
Observed live in a tb2.1 bench trial (write-compressor, tb21-twins-1):
deepseek via OpenRouter returned a truncated body, the agent died
mid-prompt with 0 turns completed, and the trial scored 0 on a provider
hiccup.
Meanwhile the same loops already retry timeouts, 429s, 5xxs, 499s, and
mid-body stream stalls — a truncated-but-complete body was the one
transient upstream fault that fell through to terminal.
## Fix
In both `post()` and `openrouter_post()`
(`crates/buzz-agent/src/llm.rs`): when the fully-received success body
fails `serde_json::from_slice`, `continue` the **existing** retry loop
instead of returning terminal — same `MAX_RETRIES` (3) bound, same
`backoff_with_jitter`. On exhaustion, the error goes through
`terminal_llm_error` so it carries cumulative duration + attempt count
like every other retried failure (previously the `json:` error carried
neither).
`post_anthropic` routes through `post()`, so
Anthropic/OpenAI/Databricks/mesh and OpenRouter are all covered.
## Why this cannot re-run a tool call
Hard requirement: tool calls are not idempotent, and this change must
not introduce any possibility of replaying one.
1. **The retry lives inside the HTTP POST helper, below the parse
boundary.** Tool calls are only ever extracted from a *successfully
parsed* response value
(`parse_openai`/`parse_anthropic`/`parse_responses`, all downstream of
these helpers' `Ok` return). A malformed body never parses, therefore no
tool call was ever extracted from it, therefore nothing downstream of it
ever dispatched.
2. **What is re-sent is the completion request itself** — the identical
`body_bytes` captured once at function entry. Sending a completion
request executes no tools; it asks the model for the next message.
3. **Same safety class as existing behavior.** The loop already re-sends
this identical request on 429/5xx/timeout/stream-stall; this adds one
more transient-fault arm to the same loop with the same bytes.
## Tests
Three new tests mirroring the existing 499/dropped-connection fixtures
(raw `TcpListener` stubs):
- `post_retries_malformed_json_body_and_succeeds` — truncated 200 body
on attempt 1, valid JSON on attempt 2; asserts success and **exactly 2**
server-side requests
- `post_exhausts_retries_on_persistent_malformed_json` —
always-truncated body; asserts exactly `MAX_RETRIES` attempts and a
terminal error carrying `json:` + cumulative/attempt context
- `openrouter_post_retries_malformed_json_body_and_succeeds` — same
recovery through OpenRouter's separate loop
Full `cargo test -p buzz-agent` green at
|
||
|
|
6e5c462ac5 |
chore(release): release Buzz Relay version 0.2.1 (#2856)
## Buzz Relay release v0.2.1 ### Changes since relay-v0.2.0: - fix(sdk): preserve self-mention p tags in message and forum event builders ([#4975](https://github.com/block/buzz/pull/4975)) ([`78c87ae20e`](https://github.com/block/buzz/commit/78c87ae20e182fffdd99744d6c9ff99df82b159c)) - feat(desktop): adding rich link previews to messages ([#3818](https://github.com/block/buzz/pull/3818)) ([`1922d49cb2`](https://github.com/block/buzz/commit/1922d49cb200a3382a91ec253f530b44dfda5f55)) - feat(relay): accept kind:30179 private managed-agent events at ingest ([#5133](https://github.com/block/buzz/pull/5133)) ([`ad923353a2`](https://github.com/block/buzz/commit/ad923353a24b784df13a7c88757d6b24ebe36299)) - fix(media): require authenticated reads ([#4610](https://github.com/block/buzz/pull/4610)) ([`769ac70b74`](https://github.com/block/buzz/commit/769ac70b741e3ad6809bff14eba29d3dd2cbd318)) - feat(identity): recover desktop identity from a signed-in phone ([#4845](https://github.com/block/buzz/pull/4845)) ([`6eb65919f1`](https://github.com/block/buzz/commit/6eb65919f1eabd46b3850c15eefab31092dd500b)) - ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes ([#3862](https://github.com/block/buzz/pull/3862)) ([`38bf642fcf`](https://github.com/block/buzz/commit/38bf642fcfa7a9fc1e06d6cf87d66ae94da29341)) - relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) ([#4542](https://github.com/block/buzz/pull/4542)) ([`e14fff74d0`](https://github.com/block/buzz/commit/e14fff74d00623acd30945eec5be366e25b0cf09)) - fix(reactions): support max-length custom emoji ([#3833](https://github.com/block/buzz/pull/3833)) ([`2ea9385015`](https://github.com/block/buzz/commit/2ea9385015fb922de2adf0a53e86fc5a21d07b90)) - fix(channels): restrict private-channel invitations ([#4612](https://github.com/block/buzz/pull/4612)) ([`efe1893dd3`](https://github.com/block/buzz/commit/efe1893dd372cfb92ed2e8a3ada2ed7b62c9477a)) - fix(workflow): bind trigger author to the signed event ([#4607](https://github.com/block/buzz/pull/4607)) ([`885bed35ee`](https://github.com/block/buzz/commit/885bed35eee3f933c48d333c8979fdbc038e98b9)) - fix(git): revoke access for banned relay members ([#4608](https://github.com/block/buzz/pull/4608)) ([`997b8caaa4`](https://github.com/block/buzz/commit/997b8caaa4c9e5af69dd8a496b4995d09a69f694)) - Define private managed agent wire protocol ([#4593](https://github.com/block/buzz/pull/4593)) ([`067c085f37`](https://github.com/block/buzz/commit/067c085f37d9dcb2f598b0e2a6b6653903364783)) - perf(relay): index channel-id lookups and skip trace-only reads ([#4647](https://github.com/block/buzz/pull/4647)) ([`bc9e6528a7`](https://github.com/block/buzz/commit/bc9e6528a7ba6007c5a25f6a0aca9c05d72e9d2c)) - Polish mobile inbox and media flows ([#4512](https://github.com/block/buzz/pull/4512)) ([`feccf4eabc`](https://github.com/block/buzz/commit/feccf4eabc23fdba94ce3537a194357ed17b197c)) - fix(git): allow deleting the default branch ([#4297](https://github.com/block/buzz/pull/4297)) ([`fc598f5f8d`](https://github.com/block/buzz/commit/fc598f5f8d70728d11d0712b9fa8e3acc44ea4c3)) - feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) ([#4020](https://github.com/block/buzz/pull/4020)) ([`b7bb15122e`](https://github.com/block/buzz/commit/b7bb15122e8a2053b545dc2210afc167f6c7a626)) - perf(relay): serve relay-membership checks from the read replica ([#4124](https://github.com/block/buzz/pull/4124)) ([`ac4fa13b8e`](https://github.com/block/buzz/commit/ac4fa13b8e4d947071d57deb6918dcf12bf74961)) - fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) ([#3998](https://github.com/block/buzz/pull/3998)) ([`5765fc74b7`](https://github.com/block/buzz/commit/5765fc74b77224f0207ddd4b41736a5ff18d333d)) - feat(relay): accept kind:30621 multi-repo projects at ingest ([#3171](https://github.com/block/buzz/pull/3171)) ([`cb9701cd30`](https://github.com/block/buzz/commit/cb9701cd30fb344bf134585634a09007f3155bfb)) - feat(relay): raise hosted community limit to five ([#3829](https://github.com/block/buzz/pull/3829)) ([`10d5a26414`](https://github.com/block/buzz/commit/10d5a26414dc90dc89fd27de74b21e105d4fa622)) - fix(relay): align NIP-11 max_limit with REQ ceiling ([#3635](https://github.com/block/buzz/pull/3635)) ([`23f0c26b1c`](https://github.com/block/buzz/commit/23f0c26b1ceba8e07bf3c160a1e08c7bda82ccd9)) - feat(relay): gate kind 30178 team-catalog reads behind the shared tag ([#3358](https://github.com/block/buzz/pull/3358)) ([`114d40d9d3`](https://github.com/block/buzz/commit/114d40d9d37f05eff83ee90347ed93fb3da512c5)) - fix(db): isolate usage metrics advisory-lock test on scratch DB ([#3670](https://github.com/block/buzz/pull/3670)) ([`dba97eecd9`](https://github.com/block/buzz/commit/dba97eecd9d8659c9c816cd6666fa6d687b6bca1)) - perf(presence): reduce heartbeat frequency ([#3783](https://github.com/block/buzz/pull/3783)) ([`bf139e8d0b`](https://github.com/block/buzz/commit/bf139e8d0bdba10df9a5adbf16843140e0a78a59)) - feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) ([#3741](https://github.com/block/buzz/pull/3741)) ([`4933672eb4`](https://github.com/block/buzz/commit/4933672eb4589e7208b312829ebddcd10dfa9dd3)) - feat(replica): portable heartbeat-token fence with snapshot-local reader routing ([#3268](https://github.com/block/buzz/pull/3268)) ([`63496cc1d4`](https://github.com/block/buzz/commit/63496cc1d4c6f1b7c613801bdcc694169dcf391a)) - fix(git): channel binding tooling + author remediation for unbound repos ([#3626](https://github.com/block/buzz/pull/3626)) ([`788b3c002b`](https://github.com/block/buzz/commit/788b3c002bd2509455444f57f8a03a054b4b496a)) - feat: configure S3 URL addressing style ([#3400](https://github.com/block/buzz/pull/3400)) ([`7012d86d52`](https://github.com/block/buzz/commit/7012d86d52fd188b27c7beedeaa132d9c1f61fa8)) - feat(tracing): correlate trace IDs in relay logs ([#3608](https://github.com/block/buzz/pull/3608)) ([`005b5b819a`](https://github.com/block/buzz/commit/005b5b819a98ce85d4d80cd81b258fb6f9b8d51e)) - fix(relay): avoid subscription lock inversion ([#3413](https://github.com/block/buzz/pull/3413)) ([`22be8bb351`](https://github.com/block/buzz/commit/22be8bb35177e27efc2dca2534df9a8dd871eae0)) - feat(cli): add users set-status command for NIP-38 profile status ([#3253](https://github.com/block/buzz/pull/3253)) ([`60158fce3e`](https://github.com/block/buzz/commit/60158fce3e670f11bb35d42627857ccaea50ff06)) - feat(relay): make Postgres pool size configurable, default 50 ([#3191](https://github.com/block/buzz/pull/3191)) ([`2ce2d71cc3`](https://github.com/block/buzz/commit/2ce2d71cc38a9657eaf344c10e07f155b8a18615)) - feat(tracing): add datastore tracing plumbing ([#2760](https://github.com/block/buzz/pull/2760)) ([`e94b9aeda0`](https://github.com/block/buzz/commit/e94b9aeda0b2272d36e3744e78680be69295b8b5)) - feat(invites): add use-limited invite links ([#3141](https://github.com/block/buzz/pull/3141)) ([`d500c2d5cf`](https://github.com/block/buzz/commit/d500c2d5cf5d9aabe0ca4ebebfcafdbe5f5b7fd3)) - feat(admin): show reported message content in report detail ([#3149](https://github.com/block/buzz/pull/3149)) ([`f069a85503`](https://github.com/block/buzz/commit/f069a8550373328babe4239ed614fcdf884721e2)) - resolve findings ([#3150](https://github.com/block/buzz/pull/3150)) ([`9b0f744804`](https://github.com/block/buzz/commit/9b0f744804697b802f7afb88947194702765c78d)) - Revert "fix(cli,relay): resolve agents by verified owner" ([#3168](https://github.com/block/buzz/pull/3168)) ([`a041e2d21e`](https://github.com/block/buzz/commit/a041e2d21e292a271fdfc26f0cdcdd0456f815c5)) - fix(cli,relay): resolve agents by verified owner ([#2615](https://github.com/block/buzz/pull/2615)) ([`c3084b36d9`](https://github.com/block/buzz/commit/c3084b36d975259f2dfeee8edc9131b40a8bce83)) - fix(security): enforce durable community ban on NIP-43 relay-admin kinds 9030-9033 ([#3128](https://github.com/block/buzz/pull/3128)) ([`e2e0079101`](https://github.com/block/buzz/commit/e2e007910114ddf7c5a4e93bb03f6afe13552e92)) - fix(security): authorize kind:9000 role changes in both directions ([#3017](https://github.com/block/buzz/pull/3017)) ([`00ecf2cac7`](https://github.com/block/buzz/commit/00ecf2cac7544d986b4eb111ad0a8b1d7560791f)) - feat(desktop): handle project work from Inbox ([#3117](https://github.com/block/buzz/pull/3117)) ([`c5c4f390b6`](https://github.com/block/buzz/commit/c5c4f390b6713256e2efb8394c59823ebad73db6)) - feat(relay): make per-owner community limit configurable via BUZZ_MAX_COMMUNITIES_PER_OWNER ([#2599](https://github.com/block/buzz/pull/2599)) ([`2a051a404d`](https://github.com/block/buzz/commit/2a051a404dcde42dddbff2a0b33f717ffe9cf999)) - feat(relay): add author-only-unless-shared read gate for kind 30175 ([#2768](https://github.com/block/buzz/pull/2768)) ([`ab3af82871`](https://github.com/block/buzz/commit/ab3af828714ab699dfc87644d234014987a4fe6b)) - fix(core): block IPv6 transition SSRF targets ([#2801](https://github.com/block/buzz/pull/2801)) ([`c26bf5945d`](https://github.com/block/buzz/commit/c26bf5945d8f2ef19746a78e80a7c1dae2ef3db9)) - fix(workflow): bypass system proxies for webhooks ([#2800](https://github.com/block/buzz/pull/2800)) ([`60a171b19e`](https://github.com/block/buzz/commit/60a171b19efd515d9213b535d52a2bcbec3ff2fe)) - fix(audit): hash created_at at the precision Postgres stores ([#2638](https://github.com/block/buzz/pull/2638)) ([`264a56a226`](https://github.com/block/buzz/commit/264a56a2260ac87350bfe1f5d3ec3d89615eb47c)) - feat(desktop): make pull request reviews actionable ([#2510](https://github.com/block/buzz/pull/2510)) ([`9081ab0ec9`](https://github.com/block/buzz/commit/9081ab0ec9c5d91548c7f5ff52eba6cca4788dd0)) - fix(relay): decompress gzip-encoded git smart-HTTP request bodies ([#2670](https://github.com/block/buzz/pull/2670)) ([`5ca36e7b91`](https://github.com/block/buzz/commit/5ca36e7b919097733868764d8e0073e99c3206c3)) - fix(sharing): preserve agent/team snapshot tEXt chunks through media sanitization ([#2438](https://github.com/block/buzz/pull/2438)) ([`b096b0a15a`](https://github.com/block/buzz/commit/b096b0a15af4c4566365c5b1efe7f39b700222ed)) - fix(relay): send 1012 restart close to all clients on graceful drain ([#2575](https://github.com/block/buzz/pull/2575)) ([`1911c69aa2`](https://github.com/block/buzz/commit/1911c69aa2912c1408bd6b21759b657458fb43af)) - fix(media): sanitize animated image uploads ([#2524](https://github.com/block/buzz/pull/2524)) ([`8f8f5fa5a4`](https://github.com/block/buzz/commit/8f8f5fa5a4b2463cdc6c2a527acb7086150cdaae)) - fix(channels): strip leading hash prefixes from names ([#2250](https://github.com/block/buzz/pull/2250)) ([`d0ab3fdb05`](https://github.com/block/buzz/commit/d0ab3fdb054e0cfedbf21e4c5143ad6c671c10cc)) - feat(relay): make Redis pool size configurable, default 16 ([#2521](https://github.com/block/buzz/pull/2521)) ([`bcc3e13069`](https://github.com/block/buzz/commit/bcc3e1306946528102bb26be9a7c41299e2f8e00)) - feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool ([#2122](https://github.com/block/buzz/pull/2122)) ([`61cc738ee8`](https://github.com/block/buzz/commit/61cc738ee8991e92563136de4b77e54cb9756420)) - feat(media): add S3-truth per-community storage sweep ([#2044](https://github.com/block/buzz/pull/2044)) ([`bd37a4d584`](https://github.com/block/buzz/commit/bd37a4d584fefc1d13ad8abadf6e890e66183072)) - feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests ([#2206](https://github.com/block/buzz/pull/2206)) ([`7e34bee62c`](https://github.com/block/buzz/commit/7e34bee62cacaa9d8a96c14d5892a471b59a1983)) - Revert "feat(relay): inventory unreachable Git objects" ([#2275](https://github.com/block/buzz/pull/2275)) ([`0fb820f9bf`](https://github.com/block/buzz/commit/0fb820f9bfbd7e19e48f9826e332920c2ee2c229)) - feat(relay): inventory unreachable Git objects ([#2264](https://github.com/block/buzz/pull/2264)) ([`3afc9dae15`](https://github.com/block/buzz/commit/3afc9dae159262220c4149e9c8add50772869318)) - relay: add author_type label to buzz_events_stored_total ([#2243](https://github.com/block/buzz/pull/2243)) ([`b9f54c43fe`](https://github.com/block/buzz/commit/b9f54c43fe2bcd0eb8fb3b76914e9aa0c31f6927)) - fix(git): make project branch workflows reliable ([#2213](https://github.com/block/buzz/pull/2213)) ([`166f27be4b`](https://github.com/block/buzz/commit/166f27be4bc1abf2d465493bf2137353045399dc)) - feat(cli): manage repository protection rules ([#2193](https://github.com/block/buzz/pull/2193)) ([`f94324598d`](https://github.com/block/buzz/commit/f94324598d84b2db9a05a3fa1f855970c4c5b575)) - feat(cli): add agents archive/unarchive/archived subcommands ([#2173](https://github.com/block/buzz/pull/2173)) ([`7d7992067b`](https://github.com/block/buzz/commit/7d7992067b2914b582b7e6d31a6174603b480b4b)) - fix(mobile): sanitize Android image uploads ([#2188](https://github.com/block/buzz/pull/2188)) ([`ee21da90bd`](https://github.com/block/buzz/commit/ee21da90bd6b1da6bfaaf22ba00749398aaa9640)) - fix(cli): paginate channel directory queries ([#2181](https://github.com/block/buzz/pull/2181)) ([`03fe19d603`](https://github.com/block/buzz/commit/03fe19d6033094ae2ec4c89c26eb23174ef53daa)) - fix(mobile): image upload fails due to unstripped metadata ([#2185](https://github.com/block/buzz/pull/2185)) ([`37f15b2001`](https://github.com/block/buzz/commit/37f15b20019169363b697aee41c99573b7bc3f24)) - perf(relay): compact Git packs before manifest limits ([#2172](https://github.com/block/buzz/pull/2172)) ([`80e0ab16b0`](https://github.com/block/buzz/commit/80e0ab16b03c656ec8def18bedc27eaf29c02867)) - perf(relay): cache Git pack hydration ([#2169](https://github.com/block/buzz/pull/2169)) ([`a4d82ec722`](https://github.com/block/buzz/commit/a4d82ec7226e685a933dbd829b6bb8bce0787b4e)) - fix(relay): bound and observe Git read operations ([#2167](https://github.com/block/buzz/pull/2167)) ([`5f7c93d9c1`](https://github.com/block/buzz/commit/5f7c93d9c12ce7894288ae47f3fe223fcff2dce3)) - relay: gate push enqueue on live leases; batch matcher pipeline (T1b/T1a-repair/T2b) ([#2145](https://github.com/block/buzz/pull/2145)) ([`e43b2d5aac`](https://github.com/block/buzz/commit/e43b2d5aac0d1f2b6b623b04f7af5a51f77da8c6)) - relay: add audit logging disable switch ([#2134](https://github.com/block/buzz/pull/2134)) ([`bf5acabdde`](https://github.com/block/buzz/commit/bf5acabdde44aa133bdcbafcf9e1a4ff752c3302)) - relay: skip TTL deadline bump for known-permanent channels (T1a write-amp) ([#2125](https://github.com/block/buzz/pull/2125)) ([`2e936d439c`](https://github.com/block/buzz/commit/2e936d439ce29182b48086f9f8a7a3ffe3b9b345)) - fix(git): carry NIP-OA delegation in auth event ([#2120](https://github.com/block/buzz/pull/2120)) ([`c12257d57a`](https://github.com/block/buzz/commit/c12257d57a54d5c1e16435440b02beb5d1c057b8)) - Route lag-tolerant reads to an optional Postgres read replica ([#2084](https://github.com/block/buzz/pull/2084)) ([`29c48883d3`](https://github.com/block/buzz/commit/29c48883d30e6feed75e33490571ca96082c6282)) - fix: recover community access visibility ([#2074](https://github.com/block/buzz/pull/2074)) ([`ca384d082d`](https://github.com/block/buzz/commit/ca384d082d9804ec53a3fd12ccbf4a0846b21d92)) - feat: proxy feedback-scoped admin attachments ([#2059](https://github.com/block/buzz/pull/2059)) ([`d7f918e3cb`](https://github.com/block/buzz/commit/d7f918e3cbcc4d30f222d0f4ae836de808f859a2)) - feat: add read-only deployment moderation dashboard ([#1999](https://github.com/block/buzz/pull/1999)) ([`68e670e001`](https://github.com/block/buzz/commit/68e670e001d2bed2cf141095926feaf482c3bed8)) - Bug-bash round 2: table scroll, Goose instructions, workflow mention wake ([#2034](https://github.com/block/buzz/pull/2034)) ([`64b8fea6dc`](https://github.com/block/buzz/commit/64b8fea6dce3be684aa0bac5dbd701e46dc7e432)) - Strip media metadata on clients and reject it at the relay ([#2006](https://github.com/block/buzz/pull/2006)) ([`5cfd69cb0c`](https://github.com/block/buzz/commit/5cfd69cb0cf1dc63d718454defe3b8a8aaf5f15b)) - [codex] Hold Git concurrency permits through streaming (BUZZ-SEC-018) ([#1916](https://github.com/block/buzz/pull/1916)) ([`7baea42abb`](https://github.com/block/buzz/commit/7baea42abbbb794e6e5ab0e9df11e2d1b0550d0b)) - [codex] Enforce shared relay admission limits (BUZZ-SEC-019) ([#1917](https://github.com/block/buzz/pull/1917)) ([`73fc0ec6cf`](https://github.com/block/buzz/commit/73fc0ec6cf58a79bfc65e42faba457bf49c2d232)) - [codex] Block banned actors from moderation commands (BUZZ-SEC-007) ([#1915](https://github.com/block/buzz/pull/1915)) ([`caa195ca58`](https://github.com/block/buzz/commit/caa195ca58ea49cf8ed9c3ede55d6a2e4ed37096)) - [codex] Fix relay WebSocket admission limits ([#1682](https://github.com/block/buzz/pull/1682)) ([`d3ce971fc7`](https://github.com/block/buzz/commit/d3ce971fc75a34162d5498c27ac4a1c30236630a)) - feat: add invite QR and mobile direct join ([#1957](https://github.com/block/buzz/pull/1957)) ([`648cbf3610`](https://github.com/block/buzz/commit/648cbf36109d97be6bd8530e77073d1c7e6008a0)) - fix(join-policy): require legal consent on hosted invites ([#1987](https://github.com/block/buzz/pull/1987)) ([`2e1577f76f`](https://github.com/block/buzz/commit/2e1577f76f5105ddacda7be884518574ca8d6b96)) - [codex] Prevent actor-tag UI impersonation ([#1931](https://github.com/block/buzz/pull/1931)) ([`c540ec9678`](https://github.com/block/buzz/commit/c540ec967869ef0f4eef90439bf70929fc74f7f6)) - Scope relay runtime state by community ([#1658](https://github.com/block/buzz/pull/1658)) ([`d52dedb06f`](https://github.com/block/buzz/commit/d52dedb06fc2c7692c6d9225c7a08b41a509633a)) - Apply optional relay join policy across join flows ([#1894](https://github.com/block/buzz/pull/1894)) ([`6c2d667575`](https://github.com/block/buzz/commit/6c2d667575cbc372ba42d26134448660fb1d2ee9)) - feat(media): require auth for relay media reads ([#1926](https://github.com/block/buzz/pull/1926)) ([`f308762852`](https://github.com/block/buzz/commit/f3087628524951de91028c9d263bcd0d0a727fab)) - feat(relay): add community unarchive endpoint ([#1908](https://github.com/block/buzz/pull/1908)) ([`6b9641db2b`](https://github.com/block/buzz/commit/6b9641db2b4709b71622b7ef0b799117a0605ca6)) - feat(relay): gate Git web GUI separately ([#1901](https://github.com/block/buzz/pull/1901)) ([`34dc7dec75`](https://github.com/block/buzz/commit/34dc7dec75285ab6f2107ce5cad170b80b92206a)) - mesh: upgrade runtime, enforce membership, add shared compute provider ([#1656](https://github.com/block/buzz/pull/1656)) ([`54638ff4bb`](https://github.com/block/buzz/commit/54638ff4bb5af2d3d3759b44118b43052f814bb1)) - Route Git scratch through configured volume ([#1884](https://github.com/block/buzz/pull/1884)) ([`2318b3096c`](https://github.com/block/buzz/commit/2318b3096c585f8d31bd43e27dc5d6305c5fe20d)) - feat(relay): gate usage metrics behind stable leader ([#1814](https://github.com/block/buzz/pull/1814)) ([`59e9821503`](https://github.com/block/buzz/commit/59e9821503a2fe23fc4630aa0f36bb252ae4566f)) - Relay mesh: cross-pod tunnel + huddle transport (buzz-relay-mesh) ([#1670](https://github.com/block/buzz/pull/1670)) ([`ccb021d713`](https://github.com/block/buzz/commit/ccb021d71339009aabedc383c8f3d8e5c23e1e42)) - feat(push): deliver accepted relay events as wakes ([#1866](https://github.com/block/buzz/pull/1866)) ([`bffbc5f22c`](https://github.com/block/buzz/commit/bffbc5f22cc80e9a07dedc622798347d598a215c)) - fix(db): resolve duplicate migration version ([#1863](https://github.com/block/buzz/pull/1863)) ([`08ad38a07f`](https://github.com/block/buzz/commit/08ad38a07f0c49bb3f20b775b8f534f1cfa529c3)) - Add private product feedback sidecar ([#1857](https://github.com/block/buzz/pull/1857)) ([`af190c93e1`](https://github.com/block/buzz/commit/af190c93e1048af64c3fbfb3831c689cb703997c)) - feat(relay): add durable community archival ([#1834](https://github.com/block/buzz/pull/1834)) ([`2b15a72675`](https://github.com/block/buzz/commit/2b15a726750dbd7437711050cd3241b679dff317)) - feat(push): add public APNs gateway ([#1770](https://github.com/block/buzz/pull/1770)) ([`1c006822e4`](https://github.com/block/buzz/commit/1c006822e4484d68e33fce14f9139c2f70ce9d66)) - feat(relay): add atomic community ownership transfer ([#1845](https://github.com/block/buzz/pull/1845)) ([`52e42ccb9f`](https://github.com/block/buzz/commit/52e42ccb9fc85445814614c72d40f346e986152b)) - Bound NIP-RS retention and search indexing ([#1771](https://github.com/block/buzz/pull/1771)) ([`1b4703021d`](https://github.com/block/buzz/commit/1b4703021dbfd37dc31845223dba9ba182e4647f)) - Add optional standalone pairing relay to Helm chart ([#1799](https://github.com/block/buzz/pull/1799)) ([`9b47c8548f`](https://github.com/block/buzz/commit/9b47c8548fd061fbb806ea8b9ddee831c19cf80e)) - fix(relay): publish membership snapshot on provisioning ([#1761](https://github.com/block/buzz/pull/1761)) ([`0950d392b7`](https://github.com/block/buzz/commit/0950d392b7a862694c95cbea1cec45985ee42996)) - feat(relay): per-community usage metrics ([#1723](https://github.com/block/buzz/pull/1723)) ([`620822899a`](https://github.com/block/buzz/commit/620822899a6373fa3a17a87815cd7cade25ed332)) - refactor(desktop): remove vestigial MCP toolsets config ([#1776](https://github.com/block/buzz/pull/1776)) ([`dfec75b3c0`](https://github.com/block/buzz/commit/dfec75b3c0b8080529e4d9089d4ed80e3902aaed)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
261c460761 |
fix(buzz-agent): recover from 400-shaped image rejections; unbound benchmark agent rounds (#5318)
## Problem
Two failure modes from the `tb21-glm52-crusoe-1` benchmark run (GLM-5.2
solo, TB2.1) wedged or killed 13 of 89 trials without the model being at
fault:
1. **Conversation poisoning on text-only endpoints.** Crusoe's
serverless `crusoeai/GLM-5.2-NVFP4` rejects any request whose history
contains an image with `400: ... is not a multimodal model`. The
recovery machinery for exactly this case already exists —
`AgentError::UnsupportedImageInput` → `replace_unsupported_images()`
strips the image blocks, marks the tool result as an error, substitutes
a text placeholder, and continues the turn. But classification only
matched OpenRouter's 404 body (`no endpoints found that support image
input`) and was only consulted on the 404 arms. The Crusoe 400 fell
through to terminal `AgentError::Llm`: the image stayed in history,
every subsequent call failed identically, buzz-acp rode its 10-retry
ladder (~40 min), and the trial idled to budget death. Measured blast
radius: **8 trials wedged, 12.7h aggregate idle-after-poison.**
2. **Bounded agent rounds in benchmark trials.** The harness default
`DEFAULT_MAX_AGENT_ROUNDS = 32` ended solo trials mid-work when turns
rotated (thinking-heavy models hit max_tokens rotation fast; 4 trials
died this way). Benchmark trials already have a wall-clock budget as the
real limit — the round cap only converts recoverable rotation into trial
death.
## Fix
- `is_unsupported_image_input_error()` also matches the verbatim `is not
a multimodal model` body. Matcher stays deliberately tight (same
doctrine as `is_context_length_error`): misclassifying a generic 400 as
recoverable would mutate history for an error that removing images
cannot fix.
- Both status ladders — shared `post()` and `openrouter_post()` —
consult it on their 400 arms and return the typed
`UnsupportedImageInput` (OpenAI-compatible providers report this as 400;
a BYOK/passthrough upstream can surface the provider's own 400 through
OpenRouter).
- Harness `DEFAULT_MAX_AGENT_ROUNDS` → `0` (unbounded —
`BUZZ_AGENT_MAX_ROUNDS=0` is the agent config's documented unbounded
value). Per-agent `budget.max_calls` in manifests still overrides.
## Acceptance
- A 400 with the image-rejection body reaches the existing image-strip
recovery path instead of wedging the session — asserted through
`complete()` (covers the return path into the convergence mapper) and at
the `openrouter_post` terminal, both proving single-attempt (a
deterministic capability rejection must never be retried).
- Ordinary 400s stay terminal `AgentError::Llm` (existing negative tests
unchanged).
- Benchmark trials run unbounded rounds by default; python tests updated
for 0-is-legal with a negative arm at -1.
## Verification
- `cargo test -p buzz-agent`: 427 + 18 + 20 + 15 + 8 + 1 + 48 passed, 0
failed (full package, 3 consecutive clean runs)
- `cargo clippy -p buzz-agent --all-targets`, `cargo fmt --check`: clean
- `uv run --extra dev pytest tests/` in harbor-buzz-orchestra: 35 passed
- Pre-push hooks (full workspace rust-tests + desktop-tauri-checks)
green on rustc 1.95.0 at head
|
||
|
|
6a17d035f7 |
Revert "fix(acp): reject unattended permission requests" (#5323)
Reverts block/buzz#4609 |
||
|
|
c7b663680a |
fix(buzz-agent): budget summarizer reasoning separately so it cannot starve the handoff summary (#5248)
## Problem
The handoff summarizer sends `max_tokens: 8192`
(`HANDOFF_MAX_OUTPUT_TOKENS`) with no reasoning budget separation. On
reasoning models, thinking tokens count against that cap: the model can
spend the entire budget reasoning, length-stop with empty `content`, and
`summarize()` — which only reads `content` — reports an empty summary.
The handoff then degrades to lossy history truncation.
Observed on deepseek-v4-flash during a terminal-bench 2.1 run
(tb21-solo-3, 89 tasks): **13 consecutive handoff attempts across 5
trials failed exactly this way** (`handoff returned empty summary;
truncating`), each burning ~3 minutes of full-cap reasoning, before a
stochastically-short reasoning run finally fit. circuit-fibsqrt alone: 5
failures, 5 truncations, then success on attempt 6. video-processing
failed its task by one frame after 3 context truncations.
## Fix
`openrouter_summary_body` now grants reasoning its own equal-sized
budget and excludes it from the response:
- `reasoning.max_tokens = max_output_tokens` — thinking gets a dedicated
budget instead of competing with the summary text
- `reasoning.exclude = true` — reasoning is never in the response body;
`summarize()` only reads `content`
- `max_tokens = max_output_tokens * 2` — the total cap covers both
budgets, so the text budget the caller asked for is actually available
for text
Non-reasoning endpoints ignore the `reasoning` object. Deliberately not
paired with `provider.require_parameters`, for the reasons documented at
`apply_openrouter_mutations` (it hard-404s valid model ids).
The prior test
`openrouter_summary_carries_neither_reasoning_nor_provider` asserted
`reasoning` absent from the summary body — that assertion guarded
against *effort-based* reasoning leaking in from config (the body is
built independently of `cfg`, which is still true and still tested:
`reasoning.effort` stays unset). Replaced with
`openrouter_summary_budgets_reasoning_separately_and_carries_no_provider`.
## Verification
- `cargo test -p buzz-agent`: 422 unit + 110 integration tests pass at
|
||
|
|
2b873cf208 |
Recover from max-token response truncation (#5223)
## Summary - treat provider `max_tokens` as an interrupted assistant response and continue the same turn with actionable feedback - discard tool calls from truncated responses, including malformed partial arguments, so they are neither executed nor replayed with invalid tool-result pairing - bound recovery to two retries while preserving normal finite `max_rounds` accounting ## Verification - `cargo fmt --all -- --check` - `cargo test -p buzz-agent` (422 unit tests plus all package integration/doc suites passed) - `cargo clippy -p buzz-agent --all-targets -- -D warnings` ## Notes The pre-push repository-wide hook also ran. Its Rust tests passed (2,270 passed, 14 ignored), but its `buzz-db` unit-test build was blocked because local rustc 1.89 is below sqlx 0.9's rustc 1.94 requirement. The affected package suite above is green on the exact pushed commit. Originating Buzz channel: `c3252dd2-0142-4e01-88c7-a2183c3960a5` Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |
||
|
|
78c87ae20e |
fix(sdk): preserve self-mention p tags in message and forum event builders (#4975)
## What users saw `buzz messages send` silently removed an explicitly supplied self-mention. The caller passed `--mention <sender-pubkey>` and received `accepted:true`, but the signed event had no matching `p` tag and `mention_pubkeys` was empty. ## Why it happened `nostr` 0.44 strips `p` tags matching the signer's pubkey by default. The codebase already opts out with `.allow_self_tagging()` for identity archive and unarchive requests, but the message and forum builders that accept mentions did not. The library therefore removed the tag during signing after the CLI had validated the explicit mention. ## What changed Added `.allow_self_tagging()` to all three event builders that accept mention tags: - `build_message` (kind 9) - `build_forum_post` (kind 45001) - `build_forum_comment` (kind 45003) An explicit mention now survives signing even when it matches the sender. ## How this was tested Added one regression test per builder. Each test signs with the same key included in the mention list and asserts that the resulting event preserves the self-referential `p` tag. Validation at `cd0f30bca`: ```text ./bin/cargo fmt --all -- --check cargo test -p buzz-sdk --lib cargo test -p buzz-cli --lib cargo clippy -p buzz-sdk -p buzz-cli --all-targets -- -D warnings ``` All 257 `buzz-sdk` tests and all 321 `buzz-cli` tests passed, and formatting and strict Clippy checks completed successfully. ## Scope and non-goals - Does not change mention validation, deduplication, or channel-member checks. - Does not change `normalize_mention_pubkeys`, which is not used by the messages-send path. - Does not add a dropped-mentions output field because the explicit tags are now preserved. Closes #4906. --------- Signed-off-by: Brad Groux <bradgroux@hotmail.com> Signed-off-by: npub17q2gdupkvswvk5kprwc7plergm4gn295uw6fe4mjyjv53ahuhtnq02jd3f <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz> Signed-off-by: Brad Groux <3053586+BradGroux@users.noreply.github.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: npub17q2gdupkvswvk5kprwc7plergm4gn295uw6fe4mjyjv53ahuhtnq02jd3f <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
1922d49cb2 |
feat(desktop): adding rich link previews to messages (#3818)
## Overview **Category:** improvement **User impact:** Link previews appear in the composer and travel as privacy-safe sender-authored snapshots, so recipients never contact the linked site merely by opening a conversation. **Problem:** Cold-cache link paste could freeze the composer before the URL painted; recipient-side unfurling leaked visits; invalid or unresolved preview work could interfere with sending or leave dead cards behind. **Solution:** Paint pasted links before starting cold resolver work, resolve only in the sender's composer, attach only complete validated snapshots at Send, and render authored snapshots without recipient fallback fetching. ## Behavior - **Cold paste stays responsive:** bare and angle-bracket URL paste paths commit the visible link before resolver work begins. - **Sender-only fetching:** metadata is resolved while composing; recipients render only the sender-authored snapshot. - **Send never waits:** pending, failed, invalid, and unsendable previews are omitted. They do not block or cancel the message. - **Terminal misses disappear:** failed, timed-out, or 404 resolver results remove the composer card while preserving visible link text. - **Display-text links work:** Markdown links such as `[review the pull request](…)` produce and send the same snapshots as bare URLs. - **Compact and Rich presentation:** Compact remains the default; Rich preserves source description line breaks and paragraphs. - **Immediate draft-wide dismissal:** clicking × immediately hides all previews for the draft, suppresses links pasted later, and emits only `["link-preview", "none"]`. No confirmation detour. Suppression resets after send or clearing the draft. - **Zero recipient fallback:** missing, stale, malformed, off-relay, unsupported, or suppressed snapshots remain ordinary visible links; recipients never regenerate them. ## Implementation - Resolve previews from deferred composer URL state so paste can paint first. - Upload finished preview media to the active community relay and snapshot only valid, sendable media references. - Atomically capture ready snapshots at submit time; never append a late preview after send. - Validate snapshot and suppression tags in desktop/native and relay ingestion, rejecting duplicate or mixed forms. - Render composer previews as stable 55px attachment cards at desktop and narrow widths. - Add deterministic E2E coverage for cold paste, ready/pending/failed/invalid previews, display-text links, multiline Rich descriptions, immediate dismissal, later-pasted links, and suppression reset. ## Validation Validated head: `9807ba8952f190e76153834abf8ab61dd40be5e2` - Push hooks passed: `check-push-org`, branch skew, desktop check, mobile tests, desktop tests, Rust tests, and desktop Tauri checks. - Focused screenshot E2E at the validated head: 5/5 passed across Compact/Rich composer and recipient states, 800px/420px geometry, display-text links, multiline descriptions, and immediate dismissal. - PR CI was triggered for this exact head and is currently running; completed checks are green at the time of this update. - Worktree is clean and both PR head and validated branch resolve to `9807ba895…`. ## Screenshots ### Compact composer | Loading | Ready | |---|---| |  |  | ### Rich composer | Loading | Ready | |---|---| |  |  | ### Responsive composer | 800px loading | 800px ready | |---|---| |  |  | | 420px loading | 420px ready | |---|---| |  |  | ### Recipient presentation | Compact | Rich | |---|---| |  |  | ### Display-text Markdown link | Composer | Recipient | |---|---| |  |  | ### Rich multiline description  ### Immediate dismissal | Before × | Immediately after × | |---|---| |  |  | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
742e8d1197 |
fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
Three pre-existing gaps in the buzz-agent observer feed fixed together
per Will's ruling ("all 3 in the current PR"):
1. **OpenAI/DBv2-GPT route** — `responses_body` never requested
`reasoning.summary`; GPT-family models billed thinking tokens but
returned `summary: []`.
2. **Anthropic/DBv2-Claude route** — `anthropic_thinking_config()` never
sent `thinking.display`; newest Claude models (Opus 5, Sonnet 5, Fable
5, Mythos 5, Opus 4.7/4.8, Mythos Preview) default to
`display:"omitted"`, returning thinking blocks with an empty `thinking`
field — observer rendered nothing.
3. **ACP v2 compliance** — buzz-agent negotiates ACP v2 but emitted
`agent_thought_chunk` and `agent_message_chunk` without `messageId`,
which ACP v2's `ContentChunk` requires (`messageId` + `content` both
required at schema head `d13d1baa`).
## Changes
**`crates/buzz-agent/src/config.rs`**
- New `ThinkingSummary` enum (`Auto`/`Concise`/`Detailed`) with
`BUZZ_AGENT_THINKING_SUMMARY` env var (default `Auto`); mirrors
`BUZZ_AGENT_THINKING_EFFORT` pattern
- `anthropic_thinking_config()` now emits `"display": "summarized"` in
both the adaptive shape and the manual-budget shape whenever thinking is
enabled
- Rewrote `is_adaptive_thinking_model` and `anthropic_thinking_config`
doc comments to match Anthropic's exact three-way per-model terminology
(doc:
https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models):
- Opus 4.6/4.7/4.8, Sonnet 4.6: **Off** — thinking OFF by default;
`type:"adaptive"` required to enable
- Opus 5, Sonnet 5: **On** — thinking on by default, can be disabled; we
still send `type:"adaptive"` to activate `output_config.effort`
- Fable 5, Mythos 5, Mythos Preview: **Always on** — thinking cannot be
disabled; we still send `type:"adaptive"` to activate
`output_config.effort`
**`crates/buzz-agent/src/llm.rs`**
- `responses_body` emits `reasoning.summary` alongside
`reasoning.effort` when effort is set (gated — no bare
`reasoning:{summary}` without effort)
- Covers both the pure-OpenAI Responses path and the DBv2 GPT-family
Responses path
**`crates/buzz-agent/src/agent.rs`**
- `agent_thought_chunk` carries `"messageId":
format!("{run_id}-thought-{round}")`
- `agent_message_chunk` carries `"messageId":
format!("{run_id}-message-{round}")`
- The two IDs are distinct (thought and assistant are two logical
messages per the ACP v2 Message ID RFD)
- `run_id` is a fresh random token per `session/prompt` invocation so
IDs are session-unique across multiple prompts
**`crates/buzz-agent/src/lib.rs`**
- `run_id` plumbed into `RunCtx` (was already generated in `run_prompt`,
just not threaded through)
**`crates/buzz-agent/tests/golden_transcripts.rs`**
- `test_acp_v2_chunks_carry_message_id` — negotiates v2, drives two
consecutive `session/prompt` calls, asserts: both chunk types carry
non-empty `messageId`; thought and message IDs are **distinct**; IDs do
**not** recur across the two prompts in the same ACP session
**`desktop/src-tauri/src/managed_agents/env_vars.rs`**
- `BUZZ_AGENT_THINKING_SUMMARY` added to `is_safe_to_reveal` allowlist
**`desktop/src-tauri/src/commands/agent_config_tests.rs`**
- Tests for `BUZZ_AGENT_THINKING_SUMMARY` allowlist entry
(case-insensitive)
## Tests added
- `parse_thinking_summary_round_trips_all_values`
- `parse_thinking_summary_unset_and_empty_yield_auto`
- `parse_thinking_summary_is_case_insensitive`
- `parse_thinking_summary_rejects_unknown_value`
- `thinking_summary_as_str_mapping`
- `responses_body_summary_present_iff_effort_set`
- `responses_body_emits_configured_summary_mode`
- `responses_body_concise_summary_mode`
- `anthropic_thinking_config_adaptive_emits_display_summarized`
- `anthropic_thinking_config_manual_budget_emits_display_summarized`
- `test_acp_v2_chunks_carry_message_id` (integration test — two-prompt
cross-session case)
## Notes
- **DBv2 gateway parity for `display`**: unverified — the DBv2 Claude
route proxies Anthropic Messages shape, but whether the gateway passes
`thinking.display` through is not confirmed. Flagged here rather than
blocking on it.
- buzz-acp and Desktop TS are unchanged — they already parse `messageId`
as optional and will pick it up from the wire automatically.
- Chat Completions and OpenRouter paths: untouched.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
b2ac66cde8 |
refactor(cli): replace probe/decider/detail split with single typed extractor (#5191)
Replaces the four-helper auth resolution path with two focused functions and adds production async tests that count relay round-trips. **Before:** `resolve_auth` called `resolve_auth_from_profile` (warn-emitting probe into a throwaway sink) → `resolve_auth_deciding` (re-classified the same profile) → `handle_auth_failure` → `auth_failure_detail` (third classification). `Option<Option<&Value>>` encoded a sentinel for unreachable state; tests exercised only the pure sync helper, not the actual fetch count. **After:** - `extract_auth(profile, target, signer) -> Result<[String;4], AuthFailure>` — pure typed extractor; `AuthFailure` now covers `NoProfile` and `NoTagsArray` inline, no separate helper needed - `resolve_auth()` is now the linear state machine: self-check → fetch + extract → on failure: fetch again → route final `Err` to `CliError::Usage` (default) or one admin warning (`--admin`). No throwaway sinks, no duplicate classification, no sentinel type. - Five async tests drive the production resolver through a counted Axum test server on `POST /query` and assert on both return value and exact fetch count: first success (1), retry success (2), double failure / no `--admin` (2 + `Err`), double failure / `--admin` (2 + `Ok(None)` + one warning), self path (0). Two parser tests pin `--admin` on both `archive` and `unarchive`. - `--admin` short help text corrected to describe when the flag takes effect (after extraction fails, not unconditionally). 341 tests passing, clippy clean, fmt clean. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
346ae8cadc |
fix(buzz-agent): escalate LLM timeouts per retry and log per-call latency (#5130)
Non-streaming LLM calls (`"stream": false`) through slow model/provider combinations routinely take longer than the fixed `BUZZ_AGENT_LLM_TIMEOUT_SECS` window (default 240 s) to return their first response byte. The retry loop then re-ran the identical 240 s bet three times, failed the turn, and the ACP harness requeued the whole turn from scratch: agents spent 30+ minutes producing nothing while every attempt died at the same wall. And because the LLM path only logged WARN lines on failure, a healthy-but-slow call was indistinguishable from a wedged one. ### Timeout handling - **Per-attempt escalation**: the per-request budget doubles after each timeout failure (`base × 2^n`, capped at `max(1200 s, base)` — `escalated_timeout()` in `llm.rs`), shared by the main `post()` loop and `openrouter_post()`. Non-timeout retryables (429/5xx/connect) do not escalate. A call that needs six minutes now succeeds on a later attempt instead of never. - **Per-request total timeouts**: enforcement moved from the client-level `read_timeout` to `RequestBuilder::timeout()` on each LLM request, so escalated budgets aren't silently floored by the shared client and each attempt's bound covers connect through body completion. Timeout error messages were updated to match the new semantics and still point at `BUZZ_AGENT_LLM_TIMEOUT_SECS`. ### Observability - One INFO line per completed LLM call: model, provider, `duration_ms`, `input_tokens`, `cached_input_tokens`, `output_tokens`. Slowness and prompt-cache effectiveness are now visible in harness logs without waiting for a failure, and `None` vs `0` token reports stay distinguishable. Handoff summarization calls log the same line with duration only. - The agent main loop wraps the call in a `session_id` tracing span so each line is attributable to a session. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
c293b3cd40 |
fix(agent): resolve oauth cache home cross-platform (#5151)
## Summary - resolve the OAuth cache home directory with the repository's platform-aware `dirs` convention - preserve the existing `.config/buzz-agent/oauth` cache layout on macOS and Linux - make the cache-path regression assertion portable across path separators ## Problem `buzz-agent` read only `$HOME` when constructing the OAuth token cache path. Packaged Windows processes do not guarantee that Unix variable, so OAuth source construction failed with `oauth cache: $HOME not set` even though Windows had a valid user profile. ## Validation Independent reviewers validated exact commit `836d820483b141b7291170cb33535ac7cb49b2eb` on Windows/MSVC with `HOME` unset and `USERPROFILE` present: - `cargo +1.94.1 clippy -p buzz-agent --all-targets --locked -- -D warnings` - `cargo +1.94.1 fmt --all -- --check` - `git diff --check f53bbd1..836d820` - `auth::` tests: 11/11 passed with `HOME` unset - full package lib target: 396 passed / 2 failed; identical-base controls classified both as pre-existing Windows failures The changed regression fails on the base with `$HOME not set` and passes on this branch. `dirs 6.0.0` was already locked by other workspace crates; the lockfile change adds only the `buzz-agent` dependency edge. Signed-off-by: Kalvin C <kalvinnchau@users.noreply.github.com> |
||
|
|
ee9690a93c |
fix(cli): emit structured JSON warning when archive/unarchive owner-auth extraction fails (#4824)
emit structured JSON diagnostics when NIP-OA owner-auth extraction fails
during `buzz agents archive`/`unarchive`
## Problem
When owner-auth extraction returned `None`, the CLI silently sent a bare
request. The relay replied with `400: missing auth tag` and the caller
had no way to know why extraction failed.
## Solution
Extract `resolve_auth_from_profile` — a sync function that owns all
three warning branches and the success path. `resolve_auth` reduces to:
self-check → fetch kind:0 → delegate.
- **Four distinct diagnostics**: no kind:0 profile / no tags array /
`classify_owner_auth_tag` failure (typed `AuthFailure` enum:
`NoAuthTag`, `AmbiguousAuthTag`, `WrongArity`, `NonStringElement`,
`InvalidOwnerHex`, `InvalidSigHex`, `OwnerMismatch`)
- **JSON format**: each fallback emits exactly one `{"warning":"..."}`
line to stderr, matching the CLI's documented structured-stderr contract
and the precedent in `channels.rs:597`
- **Relay-supplied values** (target pubkey, actual owner pubkey) pass
through `serde_json` serialization — no unescaped text
- **Admin bare path preserved**: request is always sent after the
warning; bare non-self requests are legitimate for relay admins
- **Self path unchanged**: silent, no relay query
## Boundary tests
Tests call `resolve_auth_from_profile` directly with `&mut Vec<u8>`.
Each of the three production `writeln!` calls is covered: deleting any
one fails at least one test. Success path asserts zero bytes written.
## Changes
`crates/buzz-cli/src/commands/agents.rs` only:
- `AuthFailure` enum with `message()` formatter
- `classify_owner_auth_tag` returning `Result<[String;4], AuthFailure>`
- `extract_owner_auth_tag` reduced to `#[cfg(test)]` `.ok()` wrapper
- `resolve_auth_from_profile` sync helper (testable without
`BuzzClient`)
- `resolve_auth` reduced to self-check + fetch + delegate
- 9 new boundary tests replacing the prior test-local helper
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
ad923353a2 |
feat(relay): accept kind:30179 private managed-agent events at ingest (#5133)
## What Relay-only carve-out of the ingest half of #4999: generic EVENT ingest now accepts kind:30179 (NIP-PMA private managed-agent config). One file, `crates/buzz-relay/src/handlers/ingest.rs`, 16 insertions / 15 deletions; **two semantic lines**, byte-identical to the ingest hunk of #4999 at `6f486e88`: 1. `required_scope_for_kind`: 30179 requires `Scope::UsersWrite` — same arm as its public sibling 30177 and the other owner-authored NIP-AP kinds. 2. `is_global_only_kind`: 30179 is owner-global, keyed `(pubkey, kind, d-tag)`; a stray `h` tag must not channel-scope it. The rest is import reflow plus replacing the guard test with a positive one (`private_managed_agent_kind_is_owner_scoped_global_user_data`: asserts UsersWrite scope, global-only, no h-channel scope). ## Why the guard test can be retired The removed test (`private_managed_agent_kind_remains_rejected_until_atomic_ingest_exists`) pinned a stated precondition: *"must not enter generic EVENT ingest before privacy and aggregate CAS deploy."* Both halves are resolved: - **Privacy** — the author-only read gates for 30179 shipped to main with #4593: `AUTHOR_ONLY_KINDS` membership, `req.rs` pre-filter + result gates, `count.rs`, `event.rs` fanout, and the bridge pre-filter (`bridge.rs:999-1000` returns `restricted: author-only kinds require authors=[self]` / 403). Only the author can read the event back. - **Aggregate CAS** — #4999 settled generation as **advisory**: the `g` tag is shape-validated, never relay-enforced. Last-write-wins per coordinate is the contract of record (see the kind:30179 contract blurb in #4999), so no CAS mechanism is pending on the relay side. ## Why this is inert to existing relays and clients - No production desktop code on main authors kind:30179 — the codec (`private_managed_agent.rs`) has zero non-test callers. This PR accepts a kind nobody can produce yet. - Content is opaque NIP-44 ciphertext to the relay; the relay never decrypts it. - Reads remain author-only via the already-shipped gates above. - Storage is the standard parameterized-replaceable path already exercised by kinds 30175–30178. No schema, config, or migration changes. ## Testing - Full `buzz-relay` package suite at this commit: 859 passed, 1 failed — `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` (504 vs 200), which **reproduces identically on clean main `769ac70b`** with this change stashed; pre-existing/environmental, not introduced here. - New positive ingest test passes. - Pre-push hooks green (branch-skew, rust-tests, desktop-tauri-checks). ## Relationship to #4999 #4999 (relay-primary agent config, desktop half) stays DO-NOT-MERGE pending live relay receipts + real CI; once this lands and deploys, its live test simplifies to plain `desktop-standalone` against the real relay, and #4999 rebases to drop its now-duplicate ingest hunk (identical bytes → trivial rebase). Originating thread: buzz://message?channel=06f13ed3-0557-4ac2-922c-1545dd00bf97&id=2a43b3b4933a2ea78b77088619251c061355f9b7b6dc29ea0d702193f2344149 ## Brownfield FTS note (review findings, operator-ruled non-blocking for this PR) Max and Sami independently identified that the FTS privacy skip-set is regime-dependent: migration 0008 installs the positive allowlist (`kind IN (0, 9, 40002, 45001, 45003)`) **only on an empty events table**; an already-populated database keeps the 0001/0005 negative skip-list (wrapped by 0014 to add 30350), which omits 30179 — so on such an installation this PR admits 30179 rows whose NIP-44 ciphertext gets indexed by `to_tsvector`. Sami measured both regimes against real Postgres (brownfield: 30179 INDEXED; fresh: NULL) and demonstrated the existing drift test only exercises the fresh regime. `schema/schema.sql:222`'s canonical literal is also the negative list and omits 30179. Migration dates put any relay deployed with data before 0008 landed (2026-07-13) in the brownfield class. **Scope of exposure (Sami's trace):** not a content leak — `event_visible_to_reader` / `is_author_only_event` gates hold on both search surfaces (`req.rs:725`, `bridge.rs:1770`), so foreign readers receive nothing. Lost is the storage-level NULL-tsv backstop plus FTS page budget burned on post-filtered hits. **Operator ruling (Tyler, events `1472e5b6`, `cbd368ed`):** ship this PR without an exclusion migration. Safety argument that makes this sound rather than merely accepted: main has **zero non-test 30179 writers** until #4999's desktop half deploys — no 30179 rows can exist, so nothing can be indexed in any regime while this PR is the only half live. **Additional review characterizations (Sami, non-blocking, on the record):** - *Behavioral delta enumerated:* routing triple (`required_scope_for_kind` / `is_global_only_kind` / `requires_h_channel_scope`) compared for all 65,536 kinds at base `769ac70b` vs head `77eeba6e` — exactly one row differs (30179). No other kind or client changes behavior. - *"SQL visibility before LIMIT" (NIP-PMA step 2):* no `AUTHOR_ONLY_KINDS` pushdown clause exists in `buzz-db` (only `SHARED_GATED_KINDS` has one). Author-only kinds are protected by the pre-filter (`author_only_filters_authorized`) plus post-filter omission; mixed-kind filters can burn candidate-page budget on discarded rows. Pre-existing and identical for 30300/30350 — not introduced here; noted so the NIP's step-2 checkbox is not read as fully ticked. - *Envelope validation gap:* 30179 is the only parameterized-replaceable kind at ingest with no per-kind envelope validator (codec grammar checks run in the desktop writer, not the relay). Generic limits only (256 KiB, ±15 min, pubkey==identity, d-tag bound). Self-inflicted footgun bounded to the author's own coordinate — candidate companion to the exclusion migration in the #4999 rebase, deliberately not added here. **Bound follow-up (required before/with the #4999 desktop half):** a 0014-shape additive migration (`pg_get_expr` capture + `CASE WHEN kind = 30179 THEN NULL ELSE (<existing>) END` wrap), add 30179 to the `schema/schema.sql:221` literal, and a brownfield-regime variant of the FTS drift test, per Sami's finding. Deploy-time spot check if ever wanted: `SELECT pg_get_expr(d.adbin, d.adrelid) FROM pg_attrdef d JOIN pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE d.adrelid = 'events'::regclass AND a.attname = 'search_tsv';` Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> |
||
|
|
769ac70b74 |
fix(media): require authenticated reads (#4610)
This change requires a valid signed Blossom authorization request and current relay membership for every media GET and HEAD request. It removes the unauthenticated compatibility path and updates desktop reads to send the required authorization. This blocks anonymous retrieval and access after relay-membership revocation. It does not yet bind a blob to its originating channel, so someone removed from a private channel can still read a known blob while remaining a relay member. That channel-ACL follow-up remains required before closing the full finding. ## Testing - `git diff --check origin/main...codex/security-media-read-auth` - Rebased onto `origin/main` at `5c98932` - Full CI pending Originating Buzz thread: `buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1` --------- Signed-off-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> Signed-off-by: Eli Foster <efoster@squareup.com> Co-authored-by: Eli Foster <efoster@squareup.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |