Commit Graph
2296 Commits
Author SHA1 Message Date
Hayt 80f9e6c7a8 merge desktop admin-surface @b615fbcc4f into admin-auth-e2e
* origin/wpfleger/desktop-admin-surface:
  feat(desktop): cancel-and-reopen recovery for failed enforcement
  refactor(desktop): align AdminActionRecordDto to relay wire shape
  fix(desktop): navigable processing rows, honest feedback status, relay-scoped nav gate
  feat(desktop): reopen resolved reports from the admin console
  chore(desktop): delete the unreachable community moderation queue
  feat(desktop): make the relay console the single Moderation surface
  feat(desktop): in-app admin console for relay operators

Signed-off-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-13 11:07:00 -04:00
Hayt afa9989b4d merge relay admin-api-bearer-auth @3a9bd9908 into admin-auth-e2e
* origin/wpfleger/admin-api-bearer-auth:
  fix(relay): make admin cancel atomic + ownership-fenced (cross-report 409)
  feat(relay): add reopen/cancel admin endpoints and activeAction report DTO
  fix(relay): require BUZZ_ADMIN_HOST to be a bare authority, not just parseable
  fix(relay): reject any unparseable BUZZ_ADMIN_HOST authority at config parse
  fix(relay): reject unbracketed IPv6 in BUZZ_ADMIN_HOST at config parse
  fix(relay): stop requiring operator API origin for admin console boot
  feat(relay): advertise admin API origin via NIP-11 admin_api field
  feat(relay): OPERATOR/MODERATOR role model for relay admin API with NIP-98 auth

Signed-off-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-13 11:06:55 -04:00
DuncanandWill Pfleger 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>
2026-08-13 05:47:01 -04:00
Hayt b615fbcc4f Merge remote-tracking branch 'origin/main' into wpfleger/desktop-admin-surface
* origin/main:
  Harden shared agent instruction review (#4220)

Signed-off-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-13 03:29:44 -04:00
HaytandWill Pfleger b1777b8297 feat(desktop): cancel-and-reopen recovery for failed enforcement
Wire the admin console to the relay's cancel route and align the report
detail to the ratified LATERAL semantics.

Cancel replaces the composed client-side retry. A failed enforcement is
always pre-mutation, so it is always cancellable; cancel returns the
report to `open` for a fresh resolution. Composing cancel + resolve would
imply an atomicity the relay does not provide, so the console offers a
single "Cancel & reopen" button on `failed` only — pending/enforcing are
owned by the relay recovery worker — POSTs `{actionId}` to
/reports/{id}/cancel, treats 409 as refresh-detail, and reloads rather
than consuming the response body.

Honest history: the detail LATERAL returns a succeeded action even on a
reopened (open) report — the ban actually ran, and a later reopen does
not un-happen it. The enforcement block now renders whenever an action is
present (live or history) and the resolve form gates on `isOpen` alone, so
a reopened-after-enforcement report shows both the executed-enforcement
history and the resolve form for re-triage. The old `isOpen && !activeAction`
gate wrongly stranded the operator on such a report.

Tighten the wire seam to match the relay's no-skip_serializing_if
serialization: reason/expiresAt/errorMessage are required-nullable, and
resolve/cancel return the `{status, activeAction}` envelope. Feedback
communityId/communityHost are nullable — a purged source community severs
provenance to NULL while retaining the row as operator evidence; severed
rows bucket under a "source community removed" heading and render em-dashes
in detail.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-13 03:22:05 -04:00
DuncanandWill Pfleger 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>
2026-08-13 02:43:28 -04:00
HaytandWill Pfleger 6e7242dd26 refactor(desktop): align AdminActionRecordDto to relay wire shape
Adopt the ratified relay->desktop action record JSON: add actorPubkey, actorRole, and errorMessage; replace the invented expirationSecs with the absolute expiresAt enforcement expiry; drop reportId (the parent report already carries provenance). Drop the now-absent expirationSecs from the retry body and rebuild the enforcement fixture from the wire shape.

Type-level slice only; cancel-route wiring and feedback nullable-community shapes wait on the relay batch.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-13 01:03:01 -04:00
DuncanandWill Pfleger 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>
2026-08-12 23:20:30 -04:00
DuncanandWill Pfleger 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>
2026-08-12 23:20:30 -04:00
DuncanandWill Pfleger 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>
2026-08-12 23:20:30 -04:00
DuncanandWill Pfleger 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>
2026-08-12 23:20:29 -04:00
DuncanandWill Pfleger 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>
2026-08-12 23:20:29 -04:00
DuncanandWill Pfleger 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>
2026-08-12 23:20:29 -04:00
Alex RosenzweigandGitHub a96af89526 Harden shared agent instruction review (#4220)
## Summary

- render shared-agent instructions as literal text so Markdown cannot
conceal spoiler contents, link destinations, or image sources
- reject non-reviewable Unicode controls at every agent-definition
boundary while preserving legitimate rendered emoji sequences
- verify shared catalog event IDs and signatures before trusting
authorship, coordinates, pagination, or executable content
- preserve the exact system-prompt bytes between review and execution
instead of silently stripping or normalizing content

## Security rationale

Shared system prompts are executable configuration. Previously, catalog
prompts were projected through the chat Markdown renderer, which could
hide text, replace link destinations with benign labels, and turn image
syntax into remote loads. Zero-width and bidirectional controls could
also make reviewed text differ from what the agent executes.

This change establishes a review invariant: the prompt a user sees is
the prompt the agent executes. Definitions that cannot be reviewed
faithfully are rejected rather than rewritten. Catalog events must also
pass Nostr ID/signature verification before they can claim a publisher,
coordinate, or cursor.

## What changed

- catalog instructions render as exact literal text rather than rich
Markdown
- catalog relay events are verified on a fresh wire-shaped object before
paging, coordinate selection, attribution, or projection
- forged content, pubkeys, signatures, and invalid newer heads are
ignored and cannot shadow a valid signed definition
- TypeScript catalog parsing rejects unsafe remote definitions before
they reach the UI
- shared Rust validation covers persona create/update/import, inbound
relay sync, definition-less managed-agent sync, and catalog publication
paths
- definition-less managed agents now fail closed on local create, local
update, and publication before persistence or relay retention
- linked managed agents validate their local name while treating the
persona definition as authoritative; their inert record-level prompt is
not executed or published
- names reject layout controls; prompts retain ordinary newlines and
tabs
- legitimate emoji composition is supported, including contextual VS16,
ZWJ, skin-tone, family, flag, and keycap sequences
- detached selectors/joiners, bidirectional controls, tag characters,
zero-width concealment, and other default-ignorables remain rejected
- names are bounded to 128 characters and prompts to 64 KiB
- contributor guidance documents the byte-for-byte review requirement
for future sharing paths

Validation reports the offending code point and never silently removes
it.

## E2E recording


[buzz-shared-agent-security-e2e.webm](https://github.com/user-attachments/assets/44d6b75f-0877-490f-bda4-a716fae3f700)

The recording demonstrates:

- a safe definition remains visible
- a prompt containing zero-width `U+200B` is rejected
- a name containing bidi override `U+202E` is rejected
- the prompt is preserved exactly
- spoiler, link, and image syntax remains literal and does not render or
load

## Verification

Passed locally:

- `just test`: all 10 unit and Docker-backed integration stages
- desktop frontend unit suite: 4,295 tests
- persona catalog relay unit suite: 32 tests, including forged-event and
cursor-shadowing cases
- focused Rust definition-validation coverage: 3 local create/update
tests and 6 publication-filtered tests
- complete desktop Tauri library suite after rebase: 2,263 passed, 14
ignored, 0 failed
- desktop Tauri clippy with warnings denied and Rust formatting
- complete agent Playwright spec: 34 tests
- the exact formerly failing `inbox-edit` immediate-attachment smoke
test after rebase: 1 test
- focused shared-agent publish, literal-review, hidden-control,
signature, and cross-member import Playwright coverage
- desktop E2E production build and TypeScript typecheck
- changed-file formatting/lint and file-size ratchet
- pre-commit secret scan and DCO signoff

The branch was rebased onto current `main`, which includes the upstream
attachment-button label fix. Fresh post-rebase GitHub CI is green for
every required and selected check: Desktop Core, all four Desktop Smoke
E2E shards, both Desktop E2E Integration shards and their aggregate,
Desktop E2E Relay, Desktop Build (macOS), Windows Rust, Rust Lint, DCO,
security scanners, and Desktop Release Candidate. The previously failing
`Desktop Smoke E2E (3)` shard now passes.

The repository-wide desktop check also reports existing CSS
formatting/`!important` findings in `components.css` and `terminal.css`;
neither file is changed by this PR. GitHub's Desktop Core lint and
format stage passes on the rebased branch.

---------

Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
2026-08-13 13:11:45 +10:00
HaytandWill Pfleger 8fff6dae80 fix(desktop): navigable processing rows, honest feedback status, relay-scoped nav gate
Close the desktop half of Thufir's #4768 pass-1 findings that need no relay
change. The relay-contract consumption (canonical action DTO, real cancel
route) waits on #3777.

Processing report rows were disabled in the list, but the enforcement
progress/retry/cancel UI lives only inside the detail view — so the row was
locked exactly when an operator needs to inspect a pending or failed action.
Keep processing rows navigable; the detail view already suppresses the resolve
form for any non-open report.

Feedback triage `status` was optional on the wire types and silently defaulted
to "new" when absent, misreporting a reviewed/archived entry as new after
reload. Make `status` required on both feedback DTOs and read it directly, and
type PATCH's actual `{status}` echo instead of claiming a full summary record.

The Moderation nav resolver keyed its 60s cache on pubkey alone, but NIP-11
discovery is relay-dependent — a workspace switch could serve the previous
relay's verdict. Key the resolver on the connected relay origin (and gate its
`enabled` on a resolved origin), and defer the `?section=moderation`
invalid-section redirect while the resolver is unresolved so a direct link is
not bounced before the probe can authorize.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-12 23:03:30 -04:00
Hayt fbb8b6cbb8 Merge remote-tracking branch 'origin/main' into wpfleger/desktop-admin-surface
* origin/main:
  chore(release): release Buzz Desktop version 0.5.11 (#5714)
  feat(acp): report standard adapter usage (#4950)
  fix(mobile): settle hydrated threads on latest reply (#4702)
  perf(desktop): persist channel snapshot hash (#5684)
  fix(agent): raise output limit and allow 3 recoveries (#5475)
  fix(desktop): defer foreground resume work (#5696)

Signed-off-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-12 20:48:16 -04:00
HaytandWill Pfleger 1f5e9c4c54 feat(desktop): reopen resolved reports from the admin console
Add a reopen affordance to the report detail: terminal reports
(resolved | dismissed | escalated) show a form that moves the report
back to open for re-triage via POST /reports/{id}/reopen.

Reopen is re-triage only — it does not reverse any enforcement already
applied. The copy says so, and warns more emphatically when the report
carries an actionId (an enforcement action ran while it was resolved).
A per-attempt requestId is generated once and reused on a 409 retry so a
report that has moved to processing dedupes on the relay, mirroring the
resolve flow's idempotency.

Surface confirmation on the report and feedback actions with sonner
success toasts (resolve, retry, cancel, reopen, feedback status).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-12 20:34:48 -04:00
HaytandWill Pfleger bd1b091fd5 chore(desktop): delete the unreachable community moderation queue
The community-mode moderation queue was reachable only through
ModerationQueueCard, which the previous commit removed from the nav — the
relay admin console is now the single moderation surface. Delete the card,
its triage library (lib/moderationQueue.ts) and that library's tests, and
the three hooks the card was the sole consumer of (useModerationReportsQuery,
useModerationAuditQuery, useResolveReportMutation).

The remaining moderation hooks — ban/timeout/unban and report submission —
stay: the members sidebar and message menu still consume them. With the two
report/audit read hooks gone, the write-invalidation now touches only the
surviving restrictions read.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-12 20:08:40 -04:00
HaytandWill Pfleger c538df8c38 feat(desktop): make the relay console the single Moderation surface
Wire "moderation" into settingsNavGroups under the Communities group. The
entry was defined in settingsSections but never added to a nav group, so the
sidebar never rendered it and nothing deep-linked to it — it has been
unreachable since #1617.

Repoint the "moderation" section to the relay admin console (AdminConsole
SettingsCard), retitle it "Moderation", and drop the separate "admin-console"
section id so a single surface owns relay-level trust & safety.

Gate the nav entry behind origin + probe resolution: hidden with no origin,
always visible for a saved manual origin (the Advanced control that fixes a
bad URL lives inside the surface), and — for an advertised-only origin —
visible on authorization or a transport flake but hidden on a definitive
non-admin verdict. Group reports and feedback by community for
cross-community triage, and collapse the manual origin field under an
Advanced disclosure that keeps its controls mounted.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-12 20:04:33 -04:00
9e0c6b4320 chore(release): release Buzz Desktop version 0.5.11 (#5714)
## Buzz Desktop release v0.5.11

- **Frozen main:** `4749bc7be3cdb78c2db4ce4864775ba7ab60b4cc`
- **Reviewed candidate:** `248b9d1b7666aacbcb1485b76e81de30a271ba0e`
- **Previous desktop release:** `desktop-v0.5.10`
- **Proposed immutable tag:** `desktop-v0.5.11`

This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.

The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-12 16:15:18 -07:00
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>
2026-08-12 18:18:01 -04:00
7634fe7456 fix(mobile): settle hydrated threads on latest reply (#4702)
🤖
## Summary

Mobile threads could open above the newest reply because the reply query
hydrates across relay pages while the list is still being laid out.
Ordinary thread opens now wait for authoritative hydration and late
layout before settling on the latest reply.

The initial settle is generation-guarded: if another reply arrives while
it is pending, the stale target is discarded and the current tail
becomes the target. Explicit deep links still own their requested
position, existing threads only follow remote replies when the previous
tail was visible, and local sends remain visible.

### Related issue

No matching issue found. This is separate from the channel
unread-navigation behavior in #4239.

Originating Buzz thread:
`buzz://message?channel=a9081ecd-9be0-400b-8bf9-2e8e0d385b80&id=bfb289fc53754f62f641fbf58bf2d7a9c181a3e6eb09a6ba762aeb6904b6cde4&thread=bfb289fc53754f62f641fbf58bf2d7a9c181a3e6eb09a6ba762aeb6904b6cde4`

### Testing

- Added a widget regression covering paginated hydration plus a live
reply arriving during the initial settle.
- Full mobile Flutter test suite passed; `flutter analyze` passed.
- GitHub CI passed, including the Mobile job.
- Built, installed, and launched the debug app on an iPad Pro 11-inch
(M4), iOS 18.6 simulator. An authenticated manual thread traversal was
not performed because the fresh app was not paired to a relay account.

---------

Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: loganj <loganj@squareup.com>
Signed-off-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Signed-off-by: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
2026-08-12 14:25:32 -07:00
c86443c599 perf(desktop): persist channel snapshot hash (#5684)
## Summary

- persist each relay/identity's complete channel list and server hash as
one integrity-checked snapshot
- paint the snapshot immediately on cold boot, then revalidate with
`knownHash`
- fail slow-never-wrong: malformed/legacy/partial snapshots and
mismatched not-modified responses force an unhashed full fetch
- add sidebar boot diagnostics and deterministic unit/E2E coverage for
boot, identity/relay isolation, partial writes, mismatch fallback, and
community switches

## Safety invariants

- channel list and hash are serialized in one localStorage document and
replaced together
- snapshot ownership is scoped to normalized relay URL plus identity
pubkey
- a not-modified response is accepted only when its hash exactly matches
the hash describing the available list
- any missing or impossible hash/list pairing retries
`getChannels(null)` before replacing persistence

## Validation

At exact commit `19ca25d23c434cc0b8893a93691aaf4c77794f60` with a clean
working tree:

- `cd desktop && pnpm check && pnpm typecheck` — passed (existing
informational Biome findings only)
- `cd desktop && pnpm test` — 4,723 passed
- `cd desktop && node --import ./test-loader.mjs
--experimental-strip-types --test
src/features/channels/channelSnapshot.test.mjs` — 13 passed
- `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts
--grep "cold boot paints" --repeat-each=5` — 5 passed
- `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts`
— 8 passed
- push hooks repeated desktop check/typecheck and all 4,723 unit tests
successfully

The Playwright suite uses injected bridge delays. Its roughly 0.5–0.6 s
snapshot paint and 3.0 s snapshot-to-live readings are synthetic
invariant evidence, not production desktop performance measurements.

## Measurement context

The controlled current-main investigation is documented separately in
`RESEARCH/DESKTOP_PERF_DEEP_DIVE_2026_08_12.md`; its raw local artifacts
are `.scratch/summer-perf-deepdive-results-v2.json`,
`.scratch/summer-perf-deepdive-run.log`,
`.scratch/summer-perf-deepdive-run-2.log`, and
`.scratch/summer-perf-deepdive-build-2.log`. Those original timings are
also synthetic Chromium/mock-bridge measurements and are not presented
as shipped Tauri/WKWebView or production-relay numbers.


## Latest review delta

At exact tip `e8e2b1d617aac7ea008258ad9974bbf8da9cd2eb`, storage-denial
reads fail open to the live fetch, hashless retries reject `channels:
null` before pair/persistence updates, identity-read failure enables a
hashless live fetch, and repeated consumers reuse snapshot
parsing/integrity validation by storage key + raw document. The four
remaining review nits are deferred as non-blocking follow-ups.

Validation at this tip: sidebar snapshot E2E 30/30 serial; desktop unit
suite 4,725/4,725; full push gate green (desktop check/typecheck/unit,
Rust, Tauri).

---------

Signed-off-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz>
Co-authored-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz>
2026-08-12 14:23:41 -07:00
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>
2026-08-12 16:57:11 -04:00
59f613c404 fix(desktop): defer foreground resume work (#5696)
## Summary

- defer foreground resume work until the activation task has returned, a
frame has painted, and a trailing task gets a turn
- centralize app-focus subscribers and remove the broad TanStack
`refetchOnWindowFocus` fan-out
- coalesce relay recovery and preserve an explicit deferred refresh only
for workflow data without a polling/push freshness path
- defer the notification permission native check while keeping blur and
cheap correctness signals immediate

## Why

Buzz Desktop 0.5.10 can spend roughly 1.5 seconds in the WebKit
window-focus listener/microtask checkpoint before returning to the run
loop. Focus currently fans out into query refetches, React polling
updates, relay reconnect/replay, and native work in one activation turn.
This patch establishes an interaction-first foreground boundary rather
than letting those consumers compete with the activating input and first
paint.

## Validation

- focused foreground/workflow/relay tests: 18/18 passed before commit
- `pnpm --dir desktop typecheck`: passed before commit
- pre-commit desktop check and file-size gate: passed
- pre-push desktop check, typecheck, and full desktop unit suite:
4,743/4,743 passed at `704e7b4b6618fafce655bb2b07c7a9fe0fc8c643`
- Princess Donut independent adversarial review: PASS after two
lifecycle/freshness blockers were resolved

## Manual test

1. Install the PR build and use Buzz long enough to populate channels,
home, workflows, agents, and other polling surfaces.
2. Switch to another app for 30-60 seconds.
3. Return by clicking Buzz and immediately click a channel or scroll.
4. Confirm the first interaction and paint are prompt, then confirm
channels/home/workflows refresh and a degraded relay reconnects after
the activation boundary.
5. Repeat while rapidly switching away again to verify no resume work
starts after focus has been lost.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-12 13:46:49 -07:00
HaytandWill Pfleger 142646976d feat(desktop): in-app admin console for relay operators
Add a NIP-98 client for the /api/admin/v1 relay API, surfaced as an
"Admin console" section in Settings. Relay operators view
deployment-wide moderation reports and product feedback, resolve/dismiss
reports, update feedback status, and manage the operator/moderator
staffing roster — no browser extension or bearer token required.

The console auto-discovers the relay's admin origin from its NIP-11
document (admin_api field): on mount it fetches the connected relay's
relay-information document, validates the advertised origin through
AdminOrigin::parse, and auto-probes. The manual origin field remains as
a pre-filled fallback for relays that do not advertise.

Rust:
- AdminOrigin value object: validates scheme+host+optional-port, rejects
  credentials/path/query/fragment; http:// only for loopback hosts
- AdminRoute closed enum: no IPC surface accepts arbitrary URLs or paths;
  the signed URL is byte-identical to the fetched URL
- Dedicated no-redirect reqwest client (SSRF guard: relay 3xx surfaced as
  error, NIP-98 header never forwarded across origins)
- admin_probe: typed state enum (Nip98Authorized/Denied, TokenMode,
  Disabled, NotAdminApi, NetworkOrIntercepted); Nip98Authorized only on
  authenticated 2xx
- NIP-11 admin-origin discovery command; advertised value treated as
  untrusted input and revalidated before use
- NIP-98 signing via AppState::signing_keys() (Err in recovery mode);
  one retry on 401 with a fresh event
- Response bounds enforced by Content-Length preflight and streaming
  byte counter; per-pubkey origin storage (atomic write, 0o600)

TypeScript:
- admin-console API wrappers for all Tauri commands; attachments return a
  Blob URL from caller-supplied MIME
- AdminConsoleSettingsCard: auto-discovery + probe flow, per-pubkey state,
  honest copy for every probe state
- AdminConsolePanel with Reports / Feedback / Staffing tabs
- Settings panels split under the file-size ratchet

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-12 16:02:11 -04:00
c6c6e7eca7 perf(desktop): coalesce thread-activity localStorage writes (#5693)
Each incoming thread reply drove a full `JSON.stringify` + `setItem` of
the ~600 KB thread-activity buffer. A burst of replies serialized the
whole blob once per event on the main thread, which is one of the
renderer stalls under load in the desktop-longevity arc.

This collapses the burst into a single debounced write, applying the
coalescing pattern Wes introduced for read-state persistence in #5591
(`readStateManager`) to the thread-activity path.

## What changed

- **`threadActivityStorage.ts`** — coalescing primitives:
- `scheduleThreadActivityWrite` — first-writer-wins (a pending timer is
*not* reset), 1s trailing edge. The timer reads the live buffer *at fire
time* and re-checks the loaded scope, so N replies within the window
persist exactly once with the burst's final state, and a write that
outlives a scope switch can neither land under the new key nor persist
the wrong buffer.
- `flushThreadActivityWrite` — synchronous persist + timer cancel; a
no-op when nothing is pending.
- `removeLegacyThreadActivityKey` — idempotent one-time cleanup of the
orphaned pre-relay-scoping `buzz-thread-activity.v1:<pubkey>` key.
- **`useThreadActivityPersistence.ts`** (new companion hook) — owns the
loaded scope, the write timer, the `pagehide` /
`visibilitychange`→hidden / unmount flush, and hydration + legacy
cleanup on identity/relay change. Mirrors the existing
`useObservedUnreadPersistence` sibling.
- **`useUnreadChannels.ts`** — rewired to instantiate the hook and call
`activityPersistence.schedule(...)` at both writer sites instead of
writing per event. The buffer (`threadActivityRef`) stays parent-owned;
the hook decides when it is durably persisted. Net **990** lines (was
1021), back under the 1000-line ceiling.

## Durability

`pagehide`, `visibilitychange`→hidden, unmount, and scope-reseed all
flush synchronously, so the last burst of replies survives a `Cmd+R` or
an idle reload that tears the webview down inside the coalescing window.

## Tests

- `threadActivityWriteScheduler.test.mjs` — fake-timer unit coverage:
burst→one `setItem`, live-buffer-at-fire-time, scope-mismatch rejection,
stale-scope timer abort, flush persists+cancels, flush no-op, legacy-key
removal.
- `useThreadActivityPersistence.test.mjs` — mounts the real hook via
`createRoot`+`act`: `pagehide` / visibility / unmount flush of the live
buffer, scope switch flushing A under A's key without leaking into B,
B-bucket rehydration, legacy-key cleanup, and the empty-scope write
fence.

## Related

Based on [#5591](https://github.com/block/buzz/pull/5591) (Wes) —
`perf(desktop): coalesce read state localStorage persistence`, the
proven first-writer-wins coalescing pattern this extends to thread
activity.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-12 12:06:43 -07:00
c3b0ccf383 Batch observer-store publications per relay envelope (#5680)
## Summary

- preserve the ACP observer envelope through renderer ingestion
- bulk-deduplicate/sort/fold one agent batch before one external-store
publication
- suppress publications for entirely duplicate replay batches
- cover raw history, transcript, active-turn terminal behavior, and
publication count

## Why

The harness already publishes observer frames in one-second batches.
Desktop expanded each envelope and called the global observer store once
per inner frame. Each call copied/sorted up to 3,000 retained frames and
woke every observer subscriber; the app-level active-turn bridge then
rescanned every running/deployed agent's retained buffer.

## Representative work-count profile

Controlled workload: 14 agents, 1,000 retained frames each, 24 inner
frames/envelope, 10 rounds (3,360 new frames).

| Counter | Before | After |
|---|---:|---:|
| Observer publications | 3,360 | 140 |
| Aggregate retained events revisited by a representative global
subscriber | 52,686,480 | 2,196,880 |

Both deterministic counters fall **24×**. Node wall time was
loader/JIT-noisy and is deliberately not presented as production CPU
evidence.

## Validation

Exact head `038a29f6f0ff866884e07bb66eebe87e576f6769`:

- `pnpm --dir desktop test` — 4,718 passed, 0 failed
- `pnpm --dir desktop typecheck` — passed before rebase; the rebase
changed only the base and the full suite passed on the exact head
- pre-commit Desktop Biome + file-size gate — passed

The installed v0.5.10-block process and LocalStorage database were not
restarted or modified.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
2026-08-12 11:29:19 -07:00
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>
2026-08-12 11:22:59 -07:00
Taylor HoandGitHub a8e5c89e23 fix(desktop): preserve agent mention separator after send (#5623)
**Category:** fix
**User Impact:** Typing immediately after sending to a persistently
addressed agent now continues after the agent mention instead of
corrupting it.

**Problem:** Post-send restoration passed the persistent `@Agent `
prefix through the Markdown parser, which discarded its trailing
separator and left WebKit rendering the caret at the mention boundary.

**Solution:** Restore the prefix as literal ProseMirror text, preserve
the separator, and focus a selection placed at the restored document
end. This does not expand or otherwise change the setting’s existing
scope: persistent addressed agents remain thread-only.

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

**desktop/src/features/messages/lib/useRichTextEditor.ts**
Adds a focused plain-text restoration helper that preserves trailing
whitespace while suppressing authored-update reconciliation.

**desktop/src/features/messages/ui/useMentionSendFlow.ts**
Routes non-empty post-send persistent audience restoration through the
literal-text helper instead of Markdown content loading.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Extends the real Enter-send flow to assert the preserved separator,
document-end selection, and immediate typing outside the agent mention.

</details>

## Reproduction steps

1. Open a thread with a persistently addressed agent.
2. Send a message with Enter.
3. Confirm the composer restores the addressed agent and a trailing
space.
4. Type immediately without clicking the composer.
5. Confirm the new text appears after the agent mention and the mention
remains highlighted.



https://github.com/user-attachments/assets/92f088aa-a516-48d1-acde-35e29f558f14

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-12 10:32:08 -07:00
Taylor HoandGitHub 884ed8a5d3 fix(link-previews): proxy sent preview media (#5627)
## Overview

**Category:** fix
**User Impact:** Sent link previews now reliably display their thumbnail
and favicon when the media is hosted on the relay.

**Problem:** Sent preview cards loaded relay-hosted snapshot media
directly, so authenticated relay requests could fail even though the
snapshot itself was valid. **Solution:** Rewrite snapshot media at the
shared card render boundary through Buzz's authenticated local media
proxy, preserving the original display domain and rerendering when the
proxy becomes ready.

## Changes

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

**desktop/src/shared/ui/link-preview-attachment.tsx**
Routes sent preview thumbnails and favicons through authenticated relay
media handling above the Compact/Rich fork while preserving original
metadata.

**desktop/src/testing/e2eBridge.ts**
Adds an opt-in proxy-readiness seam that deterministically re-arms the
production media lookup when released.

**desktop/tests/e2e/messaging.spec.ts**
Covers the real send, snapshot, recipient, and card-render path for
Compact and Rich previews, including fallback URLs, proxied URLs, and
decoded image content.

**desktop/tests/helpers/bridge.ts**
Exposes the opt-in media-proxy startup state to E2E tests.

</details>

## Reproduction Steps

1. Send a link whose preview snapshot includes a relay-hosted thumbnail
and favicon.
2. Inspect the sent message card in Compact mode and confirm both images
render after the local media proxy becomes ready.
3. Switch link previews to Rich mode and confirm the thumbnail and
favicon continue to render.
4. Run the focused Playwright regression:
`pnpm exec playwright test tests/e2e/messaging.spec.ts --project=smoke
--grep "sent link preview media uses the authenticated proxy"`


## Before / After

| Before | After |
| --- | --- |
| Relay-hosted preview media fails to load. | The sent preview thumbnail
and favicon render through the authenticated media proxy. |
| ![Before: sent link preview with a missing
thumbnail](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5627/link-preview-before.png)
| ![After: sent link preview with the thumbnail
rendered](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5627/link-preview-after.png)
|

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-12 10:24:21 -07:00
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>
2026-08-12 09:25:58 -07:00
63d14a0e95 fix(desktop): preserve live channel timelines (#5662)
## Summary

- restore the post-subscribe channel-window refresh that closes the gap
left by a live subscription starting at the current second
- prevent an unresolved, pageless channel window from replacing a
populated timeline cache with its first live event
- replace the invalid freshness-gate tests with a regression reproducing
the populated cache + pageless window + first live event state from the
report

## Root cause

This was a data-projection bug, not a virtualized-row failure. PR #5577
skipped the post-subscribe refresh for a fresh cache even though
`subscribeToChannelLive` starts at `since: now`, leaving events between
the cached page and subscription establishment undiscovered. A
successful but pageless companion window could then receive one live
event and project that one-row overlay over the populated message cache.
Reload fetched page zero and restored the conversation.

## Validation

Validated exact head `bfbaefe95da5452cdda3a0b5df970eb11e44f6f8`:

- focused `projectChannelWindow.test.mjs`: 9/9 passed
- pre-push: branch skew, desktop check, desktop typecheck, and all 4,715
desktop tests passed
- independent fresh-frame review: 9/10, no blockers

## Authorship disclosure

Carl implemented and is posting this change on Wes's behalf.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-12 16:19:43 +00:00
63f961c7e4 Refine channel settings and profile panels (#5574)
## Summary

- simplify channel settings into concise detail, member, canvas, and
action sections
- align human and agent profiles around shared rows, segmented tabs, and
top-level actions
- add agent runtime presentation, sticky glass behavior, and
scroll-linked action transitions

## Snapshots

### Channel settings

![Channel
settings](https://raw.githubusercontent.com/block/buzz/72d3958374a6f31c0f15912a8307101415eed084/pr-5574--01-channel-settings.png)

### Agent info

![Agent
info](https://raw.githubusercontent.com/block/buzz/72d3958374a6f31c0f15912a8307101415eed084/pr-5574--02-agent-info.png)

### Agent runtime

![Agent
runtime](https://raw.githubusercontent.com/block/buzz/72d3958374a6f31c0f15912a8307101415eed084/pr-5574--03-agent-runtime.png)

## Validation

- `pnpm -C desktop check`
- `pnpm -C desktop test` (4,604 passed)
- `pnpm -C desktop build:e2e`
- focused channel settings and agent profile Playwright tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
2026-08-12 08:25:04 -07:00
c966b862fe fix(deps): bump webbrowser to 1.2.4 for RUSTSEC-2026-0257 (#5659)
## What

Bumps `webbrowser` from `1.2.1` to `1.2.4` in both lockfiles
(`Cargo.lock` and `desktop/src-tauri/Cargo.lock`) to clear
[RUSTSEC-2026-0257](https://rustsec.org/advisories/RUSTSEC-2026-0257).

## Why

The advisory landed in the RustSec DB and flipped the `Security` job
(`cargo-deny check`) red on `main` — the same job passed on identical
lockfile state before the advisory was published. `webbrowser` 1.2.1
substitutes the URL into the Unix `BROWSER` env template *before*
tokenizing, allowing browser argument injection (e.g.
`--remote-debugging-port`). `crates/buzz-agent` calls
`webbrowser::open()` for the OAuth flow
(`crates/buzz-agent/src/auth.rs`) with an internally-constructed HTTPS
URL, so practical exploitability is low, but the gate is correctly
blocking. Fixed in `1.2.2`+.

## Scope

Lockfile-only. The `crates/buzz-agent/Cargo.toml` constraint is already
`webbrowser = "1"`, so no manifest change is needed. `webbrowser` 1.2.4
pulls in `objc2-app-kit` as a new transitive dependency; the
`windows-sys` edge churn re-unifies to versions already present in the
lockfile (no new `windows-sys` version is introduced).

## Verification

- `cargo-deny check` passes locally on the pinned toolchain (`advisories
ok, bans ok, licenses ok, sources ok`); RUSTSEC-2026-0257 no longer
reported in either lockfile.
- `cargo check -p buzz-agent` compiles clean against `webbrowser 1.2.4`.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-12 08:07:16 -07:00
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>
2026-08-12 09:42:59 -04:00
1ff98fa685 fix(desktop): launch Databricks OAuth from passive model discovery (#5607)
When a user's agent runtime is `buzz-agent` with no cached Databricks
OAuth token, the desktop app's passive model-discovery surfaces were
forbidden from launching interactive auth. Discovery failed silently, so
the model dropdown showed only built-in fallback models behind a vague
"Could not load live models for `databricks_v2`" note (reported
internally by Nick and Jose).

## What changed

Both discovery surfaces — the passive draft-form discovery and the
explicit saved-model picker — now launch the browser OAuth flow,
matching goose's behavior. The only behavioral difference between them
is cooldown handling:

- **Passive draft discovery** fires on every form-state change, so a
failed, cancelled, or timed-out sign-in records a per-host cooldown (5
min) that suppresses re-popping the browser on the next keystroke. While
the cooldown is active it returns the "sign-in required" guidance
instead of relaunching.
- **The explicit model picker** is a deliberate user action, so it
always launches and clears any stale cooldown first.

Safety rails:

- A 150s hard timeout (`AUTH_FLOW_TIMEOUT`) bounds the whole interactive
flow so an abandoned SSO tab fails discovery cleanly rather than wedging
the dropdown. Success clears the cooldown; failure and timeout both
record it.
- `AuthCooldown` recovers from a poisoned lock rather than wedging every
future sign-in on one panic.

The frontend maps the terminal Databricks sign-in states to typed,
actionable copy in `formatModelDiscoveryErrorStatus`: "sign-in required"
is a muted note pointing at the picker and `buzz-agent auth databricks`;
a failed or timed-out sign-in is a warning pointing at the explicit
retry. Other Databricks failures fall through to the existing generic
notice.

## Scope

Changes are confined to Databricks discovery and its frontend status
formatter — no `agent_models.rs` call sites are touched. The
interactive-auth helper takes an injected timeout so the
timeout/cooldown policy is unit-testable without a live browser.

## Deferred

Cooldown keys use the raw trimmed `DATABRICKS_HOST`, while the catalog
and OAuth cache normalize trailing slashes
(`crates/buzz-agent/src/catalog.rs:96`,
`crates/buzz-agent/src/llm.rs:2046`). So `https://workspace/` and
`https://workspace` share credentials but get separate cooldown entries
— an equivalent-spelling change to the host field mid-cooldown can
re-pop passive OAuth once within the 5-minute window. Self-limiting (one
extra browser launch, never auth corruption). Follow-up: a
`trim_end_matches('/')` on the cooldown key plus an equivalent-host
test, picked up with the coordinator migration if
[#5545](https://github.com/block/buzz/pull/5545) ever merges.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-12 09:35:48 -04:00
4b3570671e chore(release): release Buzz Desktop version 0.5.10 (#5613)
## Buzz Desktop release v0.5.10

- **Frozen main:** `f35930104bcbdb1332ff13735214ecb9fce1fc7b`
- **Reviewed candidate:** `1fb49103002e898607a7f6fd554cb51e94d92e08`
- **Previous desktop release:** `desktop-v0.5.9`
- **Proposed immutable tag:** `desktop-v0.5.10`

This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.

The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-11 17:36:46 -07:00
Will PflegerandGitHub f35930104b fix(desktop): remove 0.5.9+ perf regressions, speed up get_channels (#5599)
Desktop input latency regressed sharply for users on v0.5.9 and worsened
on latest main: multi-second stalls when clicking back into the app,
slow fresh boots, intermittent lockups, and scroll/mouse degradation.
Reverting to `119a84897` (pre-0.5.9) was confirmed to resolve it,
isolating the regression to that range. Profiling a live production
renderer plus a commit-level audit of the range found three independent,
additive causes — fixed here — plus a long-standing `get_channels` cost
that made every remaining refetch expensive, also addressed here.

## 1. Focus-return refetch storm (`refetchOnWindowFocus`)

#5490 wired TanStack's `focusManager` to app focus and flipped ~20 query
sites to `refetchOnWindowFocus: true`. A focus return after >60s away
fires them all within milliseconds — and a click into an unfocused
window *is* a focus return, so the burst runs before the click is
processed. That is the "click into the composer, wait 5 seconds"
symptom, and it also explains why mouse input feels worse than keyboard
(clicks arrive with focus transitions; typing happens while already
focused). A 5-second `sample` of a live production renderer caught a
single window activity-state transition consuming ~1.25s of main-thread
time, dominated by `JSON.parse` in the focus listener's microtask drain.

#5535 already established the fix pattern but applied it to only two
families (channels, home-feed). This PR extends the same 5-minute
`staleTime` discipline to the remaining families: pulse (×5), workflows
(×4), agents (×4), forum (×2), presence, user-status, custom-emoji,
channel-templates, and the persona catalog. Polling cadences and
push-invalidation paths are untouched — interval refetches and
`invalidateQueries` both bypass `staleTime`, so live-update behavior is
unchanged. Each gated family exports its focus-refetch policy as an
options object that the production hook spreads into `useQuery`, and a
`focusRefetchPolicy.test.mjs` drives a `QueryObserver` with that same
production object — locking the policy behaviorally (fresh focus return
→ 0 fetches; stale → refetch) and failing if a hook's
`staleTime`/`refetchOnWindowFocus` wiring drifts.

Four families deliberately keep tighter freshness, all surfaces where
the 5-minute gate would suppress the only refresh path and none of which
feed the app-wide storm: `repo-sync-status` keeps its fresh focus
refetch (its inline comment documents the "committed in a terminal,
switched back to the app" flow as intended); the workflow-runs list
stale-gates at 10s because a remotely-started run has no push
invalidation and its conditional 1s poll is off while the cache shows no
active runs; the workflow list queries (`useChannelWorkflowsQuery` and
the all-channels aggregate) stale-gate at 10s because they have no poll
and no relay subscription, and mutation-driven invalidation only covers
this renderer — remote workflow creates/edits/deletes surface only via
focus refetch; and the managed-agent log stale-gates at one poll tick
(30s) so returning to a live agent log refreshes immediately. Run
approvals keep the 5-minute gate under
`RUN_APPROVALS_FOCUS_STALE_TIME_MS` — their focused 10s poll already
covers freshness.

## 2. Synchronous localStorage sweep on the boot/focus path

#5453's stale-cache sweep synchronously `getItem` + `JSON.parse`s every
whitelisted localStorage entry on the main thread (multi-MB on seasoned
profiles), scheduled with a `requestIdleCallback` timeout of 1.5s that
guaranteed it landed mid-boot, and re-armed on every hidden→visible
transition — stacking it onto the exact moment the focus storm fires.
#5454's `trimSelfProfileCaches()` additionally scanned every
localStorage key on every `writeSelfProfileCache()` call (which fires
per relay self-profile delivery at boot).

Now: the first sweep waits `BOOT_SWEEP_FLOOR_MS` (30s) after startup,
the scan is time-sliced across idle callbacks, and the visibility
trigger is removed — boot-delayed plus hourly still covers the 14-day
TTL contract. The sliced sweep re-checks staleness immediately before
each removal (a key rewritten fresh mid-sweep survives), isolates
per-key storage errors so one bad entry can't strand the rest of the
snapshot, defers oversized values once rather than parsing them on a
zero-budget slice, guarantees forward progress on timeout-fired
callbacks, and cancels its scheduled slice when stopped. The profile
trim keeps a lazily-initialized memoized key count so the common
under-cap write is O(1); the full parse scan runs only when the count
exceeds a cap, resyncs if external deletions made it stale, and a failed
scan skips the trim instead of aborting the write. Sweep semantics
(rules, TTLs, eviction) are unchanged, and tests cover the scheduling,
slice-progress, error-isolation, defer-once, and trim short-circuit
behaviors.

## 3. The macOS window was never opaque

#5478's glass appearance is correctly opt-in at the CSS layer, but the
compositor cost was baked in deeper than its native `on_webview_ready`
transparency call: the main window is declared `"transparent": true` in
`tauri.conf.json` (added for the original glass work in #1671), which
makes tao call `NSWindow.setOpaque(false)` at creation and resolve every
later `set_background_color(None)` to `clearColor` — and no runtime
`setOpaque(true)` path exists through tauri, while wry's runtime
background setter can only force the WKWebView's `drawsBackground` off,
never back on. So "restore the platform default" was unreachable: every
launch, glass or not, ran with a non-opaque NSWindow, defeating
WindowServer's opaque-window compositing fast path and forcing full
window compositing every frame — compounded by the existing
`backdrop-blur` chrome overlapping the scrolling timeline. This matches
the compositor-shaped symptoms (scroll and pointer input degrading
first).

The window is now created opaque (`"transparent": false`) and the
NSWindow layer is never made transparent at runtime. Glass never needed
a transparent window: behind-window `NSVisualEffectView` vibrancy
renders inside opaque windows (this is how Finder and Notes draw vibrant
sidebars); it only requires a transparent WKWebView canvas, which the
`set_window_vibrancy` enable path already establishes at runtime
(`macos-private-api` compiles that in independent of the window flag).
Enabling glass installs the vibrancy layer and then makes only the
webview canvas see-through; disabling clears the vibrancy layer — the
canvas may stay non-drawing afterwards (wry's flag is one-way at
runtime), which is harmless because glass-off CSS paints fully opaque
above an always-opaque NSWindow. The boot-path first-frame backing
writes touch only the NSWindow backing color and are therefore inert to
glass state regardless of how they order against the `ThemeProvider`'s
vibrancy call on a persisted-glass-on cold boot. Glass-off users (the
default) get an end-to-end opaque window from boot for the first time.

## 4. `get_channels`: serial round-trips and a multi-MB payload on every
refetch

The stale gates in (1) cut refetch frequency; this cuts the cost of the
refetches that legitimately remain (boot, and focus returns after more
than 5 minutes away — previously still a multi-second stall).
`get_channels` made ~8 fully serial relay round-trips (~3.2–3.6s at
1,100+ channels), then shipped the full `ChannelInfo` list — including
every channel's member pubkeys — across IPC, where the renderer's
`JSON.parse` of the multi-MB payload froze the main thread (the ~1.25s
stall captured in the live sample).

- **Concurrent stages**: the membership chain, the open-channel
directory scan, and the hidden-DM snapshot run concurrently, as do the
member-count and last-message queries that follow. The critical path
drops from ~8 sequential round-trips to 2 phases. Filters, limits,
pagination, and merge semantics are unchanged.
- **Not-modified short-circuit**: the command now takes a
client-supplied content hash (FNV-1a 64 over the channel list,
canonicalized by id and excluding `last_message_at`) and omits the
channel list from the response when nothing else changed. Last-message
timestamps — which change on nearly every message anywhere — ship as a
small separate map that the client overlays onto its cached list with
reference preservation, so React Query's structural sharing also skips
downstream re-renders. On a typical refocus the renderer parses
kilobytes instead of megabytes. The hash is stored in the query cache
itself, tying its lifecycle to the data it describes so a community
switch can never leak a stale hash.

The E2E mock bridge speaks the new payload shape — including the
complete `last_messages` map the client treats as authoritative — and
hash canonicalization plus overlay reference-preservation are
unit-tested on both sides.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-11 19:47:19 -04:00
9203bf60ee perf(desktop): coalesce read state localStorage persistence (#5591)
Follow-on to #5453/#5454's localStorage work — found while investigating
app-slowness reports on a real profile.

## Problem

`ReadStateManager.persistLocalState()` serialized and rewrote **all
three** read-state localStorage blobs (`buzz.channel-read-state.v2`,
`.publishable.v1`, `.source-created-at.v1`) synchronously on every
context advance. On a real profile (1,643 contexts, ~450K chars across
the three blobs) this produced ~880KB of localStorage sqlite WAL growth
per 30 seconds at idle, with writes every ~5s — steady main-thread
serialization + sync IPC for no user-visible benefit. Observed WAL size
on the affected profile: 94–114MB.

## Fix

- Local persistence coalesced behind a **1s trailing-edge timer**: a
burst of N advances produces one `writeStoredReadState` (one write per
blob).
- Pending dirty state **flushes synchronously** on `pagehide`, hidden
`visibilitychange`, `destroy()`, and before each relay publish — disk is
current before any relay event goes out.
- Hydration still persists immediately. Publish debounce (5s), merge
logic, and blob formats unchanged (`DEBOUNCE_MS` renamed to
`PUBLISH_DEBOUNCE_MS` only).

## Accepted residual

A hard kill (SIGKILL/power loss — not webview teardown) inside the 1s
window loses ≤1s of local read-state advances; relay max-merge bounds
the effect to a message flickering back unread. On the record per
review.

## Validation

- `readStateManager.test.mjs`: fake-timer/mock-storage coverage —
exactly one 3-blob write per burst (zero before the timer fires),
hidden-flush cancels the timer and persists, hydrate persists
immediately, pre-publish flush. Suite 26/26.
- Push gate at the pushed commit: desktop check, typecheck, full desktop
unit suite 4,670/4,670.
- Independent adversarial FULL REVIEW: **APPROVE** at tree `371a02cf`
(commit metadata rewritten afterward for attribution; tree identical) —
all six `persistLocalState` call sites traced, lifecycle/leak checks
(StrictMode remount, pubkey change), no external readers of the blob
keys.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-11 16:41:35 -07:00
Taksh KothariandGitHub 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>
2026-08-11 23:03:18 +00:00
d9dc76c0aa fix(desktop): bound initial timeline retention (#5603)
## Summary

- let Virtua own the initial visible timeline range instead of passing
every loaded row to `keepMounted`
- populate the existing bounded retention window after the virtualizer
reports its first settled viewport
- cover a 10,000-row timeline to prevent an all-history initial mount
regression

## Why

`useTimelineRetention` initialized its retained-key set with every
loaded timeline key. Those indices were passed to Virtua's
`keepMounted`, effectively defeating virtualization during initial
channel positioning until `onScrollEnd` pruned the set.

On a large real channel this grew WebContent into multiple gigabytes and
blocked the renderer main thread for 20+ seconds while WebKit laid out
and painted the retained rows. Starting with no retained rows restores
Virtua's visible-range mount; the existing reader-neighborhood and
visual-tail retention is populated once the viewport is measured.

## Validation

- `node --import ./test-loader.mjs --experimental-strip-types --test
src/features/messages/ui/useTimelineRetention.test.mjs`
- pre-push hook at `8e86a189de7e9a8f2cb119396c8f912ed9dacd6e`:
branch-skew, desktop-check, desktop-typecheck, and all 4,671 desktop
tests passed
- manual ablation against PR #5599 on the affected profile: catastrophic
channel-switch stalls disappeared

## Authorship disclosure

Carl implemented and is posting this change on Wes's behalf.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-11 15:59:36 -07:00
Taksh KothariandGitHub 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>
2026-08-11 15:44:28 -07:00
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>
2026-08-12 08:04:54 +10:00
cf03bd7c37 Improve desktop search scoping (#5306)
## What changed

- unify Cmd+K and channel Cmd+F around a removable channel or
conversation scope
- add conservative fuzzy matching for people and channels while
preserving exact-match ordering
- make scoped message search complete for one-character queries and
expose up to 40 scrollable results
- keep the pre-scope channel or DM action in the normal results flow so
it scrolls away with the list

## Validation

- desktop TypeScript typecheck
- desktop text-size and file-size guards
- focused fuzzy-search unit tests (24 passed)
- focused search Playwright coverage (7 passed), including channel and
DM copy, one-character results/no-results, 40-result scrolling, and the
non-sticky scope action
- desktop E2E build
- visual review of channel, scoped, expanded-results, and DM states

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-08-11 11:11:57 -07:00
cd2aa5c12d Add glass appearance and cohesive settings (#5478)
## Summary

- add an opt-in native glass sidebar with opacity controls and live
theme previews
- refine sidebar spacing and Buzz-only active rows while preserving
production defaults
- unify settings section cards, subtitles, and agent runtime rows

## Validation

- repository format, lint, type, and file-size checks
- 4,538 desktop tests and 2,270 native desktop tests
- desktop and web production builds
- 1,261 mobile tests in the completed full gate
- focused Playwright appearance, sidebar, settings, pairing, and runtime
coverage

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
2026-08-11 10:25:23 -07:00
b0795a10ea Add Send to channel for thread messages (#5305)
## Summary

- Share eligible self-authored or owned-agent thread messages into the
parent channel as new top-level messages.
- Link the shared message back to the exact root thread with a semantic
channel label and excerpt.
- Add a dedicated channel-arrow icon plus ownership and navigation
coverage.

## Validation

- Desktop lint, size, and text guards
- Desktop TypeScript build and all 4,543 unit tests
- Focused Playwright send-to-channel and thread-link navigation tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-11 10:18:54 -07:00
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>
2026-08-11 09:51:42 -07:00
d3ec831e0c fix(desktop): preserve fresh channel timelines (#5577)
## Summary

- skip the channel subscription catch-up request when the authoritative
channel window was fetched successfully within the existing five-minute
freshness period
- keep the responsive deferred skeleton for populated channel switches
instead of briefly rendering empty-channel actions
- preserve a real empty-channel intro across the first appended message
only after React has committed that empty state

This is intentionally narrow. It does not claim to solve the separate
sidebar startup cost or general main-thread stalls found during the
investigation.

### Related issue

N/A — no matching open issue or PR found.

### Testing

- pre-push desktop gate on `f1be6beea90b9715e04e5fc65cc5cfbe8210e0d9`:
  - desktop tests: 4,621 passed
  - desktop check: passed
  - desktop typecheck: passed
  - branch-skew: passed
- focused cache/surface/lifecycle tests: 62 passed
- manual diagnostic trace after rollback:
  - 16/16 channel revisits skipped catch-up refresh
  - 0 revisit refresh starts
  - 0 populated-channel empty/intro flashes
  - cached switches retained the deferred skeleton-to-list path

No screenshot: the regression is a transient channel-switch state and
request behavior, covered by lifecycle tests and the diagnostic trace
rather than a stable visual diff.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-11 09:37:12 -07:00
4935724494 fix(desktop): suppress fresh focus-return refetches for channels and home-feed (#5535)
Fixes the 0.5.9 sluggishness Wes reported in app-slowness-mac (channel
list slow, content slow).

## Problem

#5490 (shipped in 0.5.9) flipped ~20 query sites to
`refetchOnWindowFocus: true` and wired TanStack focusManager to app
focus. Instrumented at that exact commit: regaining focus after >60s
away fires **7 query fetches within 2ms**, including `get_channels`,
which settles at **~3.6s** (production probe: median 3.2s at 1,133
channels — 8 serial round-trips, 1,133-filter last-message batch). In
0.5.8 this burst was zero by configuration. Net: a burst of fetch/parse
contention exactly when the user returns to the app.

Relay ruled out: v0.2.1 small reads are 2–4ms upstream; nothing in
v0.2.0..v0.2.1 degrades the query path. The O(N) `get_channels` design
is a pre-existing issue (June analysis) — this PR fixes the new stampede
that made it user-visible.

## Fix

Raise `staleTime` to 5 minutes on the two expensive focus-refetch
families — `channels` and `home-feed` — so a focus return inside that
window serves cache instead of refetching. `refetchOnWindowFocus: true`
only refetches stale queries, so genuinely old data still refreshes on
return.

Unchanged: focused polling cadence (60s channels / 30s home-feed;
interval refetches ignore staleTime), #5490 blur quiescence (no changes
to `useDocumentVisible.ts`/`queryClient.ts`), all push-style
invalidation paths (`invalidateQueries` bypasses staleTime), and
channels cold-start revalidate (`initialDataUpdatedAt: 0`).

## Validation

- New regression test
`desktop/src/features/home/focusRefetchPolicy.test.mjs` (4/4): fresh
focus return → 0 fetches; stale → 1; polling constants locked.
- Pre-push gate at the reviewed tree: desktop-check, desktop-typecheck,
full desktop-test **4588/4588**.
- Independent adversarial review (Beth): APPROVE at tree `4e2546ec` —
verified fresh-skip/stale-refetch against query-core 5.100.14 source,
polling-cadence via browser-simulated probe, side-effect sweep of all
invalidation paths clean. Sole CHANGE was commit trailers, fixed by
amend (tree unchanged).

## Known residual

Focus returns after >5min still fire the full burst including the
~3.2–3.6s `get_channels`. This cuts stampede frequency, not magnitude —
the O(N) `get_channels` relay path (RESEARCH/GET_CHANNELS_SLOWNESS.md)
is the follow-up that fixes magnitude.

Diagnosis: Summer (focus profiling) + Morty (relay probe); implemented
by Meeseeks; reviewed by Beth; integrated by Rick. Thread:
app-slowness-mac
e78fad29380d9a0974c9d673910450994a228781ddce133a8cedbd90504d95be.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz>
2026-08-11 09:07:39 -07:00