Repository navigation
Update desktop icon and persist window state - #9
Merged
Merged
Conversation
tlongwell-block
added a commit
that referenced
this pull request
Mar 10, 2026
* origin/main: Add desktop Home feed (#12) Add desktop Playwright e2e harness (#11) Update desktop icon and persist window state (#9) feat: add channel creation flow (#8) Improve message markdown display and formatting (#7) feat(desktop): connect chat to relay (#6) docs(readme): clarify desktop setup (#4) feat: add desktop app (#3) # Conflicts: # crates/sprout-test-client/tests/e2e_rest_api.rs
tlongwell-block
added a commit
that referenced
this pull request
Mar 11, 2026
* origin/main: feat: soft-delete for events/channels, enriched API responses, NIP-29 group management (#17) feat: Channel management, messaging, threads, DMs, reactions, and NIP-29 support (#16) Improve chat scrolling and multiline composer (#14) chore: remove redundant inline comments across all crates (#13) Initial backend revisions, workflow expansion (#5) Add desktop Home feed (#12) Add desktop Playwright e2e harness (#11) Update desktop icon and persist window state (#9) feat: add channel creation flow (#8)
wpfleger96
added a commit
that referenced
this pull request
May 22, 2026
…iew findings The original implementation created a second parallel Tauri command (discover_all_acp_providers) alongside the existing one to avoid changing the return type. This produced two commands, two hooks, two query keys, and two raw type converters. Consolidates into a single command returning the full catalog, with a useAvailableAcpProviders hook that type-narrows for callers needing non-null command/binaryPath. Also fixes: pipe deadlock in install command (#1), UTF-8 truncation panic (#2/#4), adds install concurrency guard (#11), exact provider ID match (#15), error display stdout fallback (#5), success banner suppression when already available (#12), misleading re-run text (#13), IIFE refactor in PersonaDialog (#14), hidden internal query lift (#7), configurable e2e mocks (#9), shared raw type exports (#8), and classify_provider unit tests (#10).
wpfleger96
added a commit
that referenced
this pull request
May 22, 2026
…iew findings The original implementation created a second parallel Tauri command (discover_all_acp_providers) alongside the existing one to avoid changing the return type. This produced two commands, two hooks, two query keys, and two raw type converters. Consolidates into a single command returning the full catalog, with a useAvailableAcpProviders hook that type-narrows for callers needing non-null command/binaryPath. Also fixes: pipe deadlock in install command (#1), UTF-8 truncation panic (#2/#4), adds install concurrency guard (#11), exact provider ID match (#15), error display stdout fallback (#5), success banner suppression when already available (#12), misleading re-run text (#13), IIFE refactor in PersonaDialog (#14), hidden internal query lift (#7), configurable e2e mocks (#9), shared raw type exports (#8), and classify_provider unit tests (#10).
wpfleger96
added a commit
that referenced
this pull request
May 22, 2026
…iew findings The original implementation created a second parallel Tauri command (discover_all_acp_providers) alongside the existing one to avoid changing the return type. This produced two commands, two hooks, two query keys, and two raw type converters. Consolidates into a single command returning the full catalog, with a useAvailableAcpProviders hook that type-narrows for callers needing non-null command/binaryPath. Also fixes: pipe deadlock in install command (#1), UTF-8 truncation panic (#2/#4), adds install concurrency guard (#11), exact provider ID match (#15), error display stdout fallback (#5), success banner suppression when already available (#12), misleading re-run text (#13), IIFE refactor in PersonaDialog (#14), hidden internal query lift (#7), configurable e2e mocks (#9), shared raw type exports (#8), and classify_provider unit tests (#10).
kaalph
pushed a commit
to kaalph/buzz
that referenced
this pull request
Aug 21, 2026
…iew findings The original implementation created a second parallel Tauri command (discover_all_acp_providers) alongside the existing one to avoid changing the return type. This produced two commands, two hooks, two query keys, and two raw type converters. Consolidates into a single command returning the full catalog, with a useAvailableAcpProviders hook that type-narrows for callers needing non-null command/binaryPath. Also fixes: pipe deadlock in install command (block#1), UTF-8 truncation panic (block#2/block#4), adds install concurrency guard (block#11), exact provider ID match (block#15), error display stdout fallback (block#5), success banner suppression when already available (block#12), misleading re-run text (block#13), IIFE refactor in PersonaDialog (block#14), hidden internal query lift (block#7), configurable e2e mocks (block#9), shared raw type exports (block#8), and classify_provider unit tests (block#10).
BradGroux
pushed a commit
to BradGroux/buzz
that referenced
this pull request
Aug 23, 2026
4 tasks
birdblues
added a commit
to birdblues/buzz
that referenced
this pull request
Sep 5, 2026
…side Review follow-up (Codex + Grok on e4bf6fa). Both flagged the same blocker: the WebView loaded the app door over plain HTTP and trusted the response CSP, so a LAN MITM (or a header regression) could strip the policy and restore fetch/XHR/WebSocket/subresource exfiltration — the navigation delegate only sees navigations. Grok also warned that iOS 17+ ATS may block raw private-IP HTTP in WKWebView regardless of NSAllowsLocalNetworking. Both go away by never letting the WebView touch the network, the way the desktop proxy works: - shared/relay/app_content.dart: fetchAppDocument — Authorization header, no redirects (a 3xx fails; a custom header must never follow one), text/html only, 8 MiB cap, timeout; stampSandboxCsp — the relay/desktop policy minus `sandbox` (not expressible in <meta>) inserted as the first element after a leading doctype so no script can precede it. - app_webview_page.dart: loadHtmlString with no base URL → about:blank, opaque origin, no storage. Navigation is one-shot: exactly the first main-frame about:blank load; reloads, window.open, same-URL posts and every subframe are prevented (decideAppNavigation, Codex block#2). Retries are generation-fenced so a stale callback cannot overwrite the current load (Codex block#4). Before running, Dart asks the native side whether the WebRTC hook is installed and fails closed otherwise (Codex block#3, Android included). - SandboxWebViewHardening.swift: hook loadHTMLString(_:baseURL:) instead of load(_:) — the page's only entry point now; `installed` is set only after the exchange succeeds; kill list gains WebTransport and webkitGetUserMedia (Grok block#4; desktop script updated for parity). - Info.plist: the ATS exception is removed — nothing in the WebView loads from the network any more. - message_content.dart: an `![…]()` reference to an HTML app never reaches the image decoder; without a door it renders an inert download pill (Codex block#6). New `allowAppCards` flag turned off on the forum post card and compose-note preview, which crop with a box instead of maxLines and could hide a card's safety chrome (Codex block#5). authorLabel wired on forum thread posts/replies and pulse notes (Codex block#7, Grok block#7). - relay_info.dart: a failed NIP-11 lookup while connected retries after 30 s instead of waiting for the next reconnect (Grok block#9). - AppCard: the preview is a plain tap target; Run is the one announced action (Grok block#8). - docs/sandboxed-apps.md: threat-model rows and the mobile table updated; iPad step 3 no longer checks ATS, the probe must see location.href == about:blank. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UAm5qbNPszvcwnMMbfGVGa Signed-off-by: dhseo <birdblues@mac.com>
TheSentinel454
added a commit
that referenced
this pull request
Nov 5, 2026
…er pod (#9132) ## Problem A routed read that misses the reader's 150ms acquire budget (`reader_acquire_timeout`) falls back to the writer, whatever the read is. Under a burst of subscription-history reads (REQ history, channel and thread windows, feeds, search hydration, COUNT), the reader pool fills, and every miss moves a heavy history query onto the writer. There it competes with the authentication, tenant-resolution and moderation lookups that only the writer serves, and their latency climbs with it. Nothing bounds how many history reads a pod sends to the reader at once, so the reader queue grows without limit as well. Separately, the compatibility families `buzz_db_pool_acquire_wait_seconds` and `buzz_db_pool_acquisitions_total` have no `operation` label, so a breakdown by operation reports every sample as unlabelled. ## Changes 1. **History reads no longer spill onto the writer when the reader is busy** (`Db::route_read`). `route_read` now returns `Result<RouteDecision>`. For `ReaderOperation::SubscriptionHistory`, a `reader_acquire_timeout` on a saturated reader pool rejects the read with `sqlx::Error::PoolTimedOut`. Saturated means every connection the pool may hold exists and none is idle (`Db::reader_saturated`). The check is needed because sqlx also reports a reader that refuses dials as `PoolTimedOut`, and that reader is down, not busy. The relay already answers that with its generic retryable `CLOSED` `error: database error`, so there is no new client contract. A reader that is wrong rather than busy (unreachable, validation failure, proof timeout, stale token) and prechecks that never reach the reader still fall back to the writer. Authorization reads (`is_relay_member`) keep the writer fallback unchanged. 2. **Per-pod cap on reader-served history reads** (`runtime/history_reads.rs`). A FIFO `tokio::sync::Semaphore` admits at most the reader pool size less one fifth (at least one) history reads at once. For example, the default pool of 20 admits 16. The remaining connections are normally free for authorization reads. A read waits at most 1s for a permit and is otherwise rejected the same way. The permit is owned by the reader transaction (`ReaderTx`) and released when the transaction is dropped; routed reads drop it before re-running a failed replica read on the writer. That holds whether the transaction is a per-call routed read or a `ReadSession` the relay keeps for follow-up queries. The cap bounds history transactions, not connections: sqlx returns a dropped transaction's connection asynchronously through the reader release check. Released connections can therefore briefly occupy the free slots, for one round trip normally and at most the 5s release timeout when sockets stall. In that window, authorization reads fall back to the writer as before, and history reads on the saturated pool are rejected, not spilled. 3. **Rejections are observable**: `buzz_db_route_decision{decision="rejected",reason="history_read_cap"|"reader_acquire_timeout"}`. 4. **Legacy pool families gain `operation`.** The population is unchanged, and summing over `operation` gives the original series. 5. **HTTP reads answer 503 `database unavailable` when the database can't be reached** (`api::db_read_error`). This covers a pool timeout, closed pool or socket failure on `POST /query`, `/count` and every bridge `/query` branch that serves a history read (channel window, the three feeds, thread replies), including a shed history read. These used to return a generic 500. The thread-window path already answered 503 for pool timeouts; this applies the same mapping to the shared helper. Pool sizes are unchanged on purpose. A larger pool adds lock contention on the writer rather than relieving it. Docs: `deploy/charts/buzz/README.md` (operator contract) and `PLANS/REPLICA_FULL_READ_ROUTING_DESIGN.md` (fail-closed exceptions). ## What got simpler The concept count is roughly flat. Instead of a caller-side permit, the permit lives in the one type every replica transaction already flows through (`RouteDecision::Replica` / `ReadSessionInner::Replica`). The 11 routed call sites change only by a `?`, and the compiler enforces that change: a call site cannot silently fall through to the writer on a rejection. No new error variant, CLOSED reason or env var. ## Tests - `postgres_tests::saturated_reader_spends_one_acquire_budget_and_rejects_history_reads` (rewrites the former `routed_fallback_spends_one_acquire_budget_when_aurora_cache_is_cold`). A size-1 reader is held saturated with a cold Aurora cache. A history read (`count_events_routed`) is rejected with `PoolTimedOut` within one acquire budget, records `rejected/reader_acquire_timeout`, and releases its permit. An authorization read on the same reader still falls back as `writer/reader_acquire_timeout`. - `postgres_tests::match_nothing_reads_take_no_connection` (unchanged): its control still requires a history read against an unreachable reader to fall back to the writer. It caught the first cut of this change, which rejected that read. - `postgres_tests::history_permit_follows_the_read_session_and_a_full_gate_rejects`: a held replica `ReadSession` keeps its permit. With the gate full, the next history read waits the full permit budget and is then rejected as `rejected/history_read_cap`, with no writer route recorded. Dropping the session frees the permit and the next read routes to the replica. - `history_reads::tests`: a cap sizing table, plus bounded-wait rejection and reopen under paused time. - `observability` label tests now require `operation` on both legacy families. - `api::db_read_error_tests::unavailable_database_maps_to_retryable_503`: pool timeout, closed pool and I/O map to 503 `database unavailable`. Other errors stay 500. - `postgres_tests::shed_history_read_maps_to_503_on_every_bridge_branch`: with the reader saturated, `/query` channel-window, mentions, needs_action, activity and thread-reply requests each answer 503 `database unavailable` (each serves 200 beforehand). - Mutation-checked on Blox: re-enabling the writer spill, forcing the saturation check either way, skipping the gate, dropping the permit early and removing the legacy label each fail a test. ## Residual - During a reader failover whose old sockets stall, both slots sit in the release check, so the pool reads as saturated. History reads are shed as retryable for up to the release bound (5s) instead of loading the writer. `stalled_reader_sessions_release_their_slots_for_new_connections` now retries through that window and still requires recovery onto the reader. - A reader that accepts no TCP at all can fill the pool with in-flight dials and look saturated. Its history reads are then rejected rather than moved to the writer, which is the same outcome as a full gate. - Shedding relies on clients backing off on a retryable `CLOSED` (RPR #9). WebSocket clients already treat `error: database error` as retryable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
TheSentinel454
added a commit
that referenced
this pull request
Nov 6, 2026
…ia feature flags (#9170) ## Goal Give operators a way to relieve database pressure in seconds, without restarting relay pods. Today every concurrency limit is an environment variable, so changing one means a rollout. During an incident, that rollout's restart sets off a reconnect storm of its own. ## What changes **Two runtime-tunable caps.** Each is read from an integer feature flag in a new process-wide evaluation context, and re-read every 5 s: | Flag | Caps | Ceiling | |---|---|---| | `buzz.relay.concurrency.history-reads` | subscription-history reads holding a reader connection (the #9132 gate) | the existing `pool − pool/5` share, which keeps readers free for authorization | | `buzz.relay.concurrency.auth` | verified NIP-42 AUTH frames running their database checks | none (unbounded today) | - **Flags can only tighten.** A positive value is clamped to the ceiling. Zero, a negative value, no value, or a provider that never initialized leaves the gate at its ceiling, which is today's behavior. Once LaunchDarkly has delivered values, they stay in force through a later outage, so a cap tightened during an incident survives a provider blip. - **Lowering never cancels admitted work.** A lowered gate admits nothing new until the work in flight falls under the new limit. - **Visibility:** `buzz_concurrency_limit{gate}` reports the applied limit (`0` means no limit), and each change logs a line. **One resizable gate.** `HistoryReadGate` becomes `buzz_db::ConcurrencyGate`, a FIFO semaphore whose limit can move anywhere in `1..=ceiling`: - Lowering it forgets free permits and records any shortfall as debt. - A permit released while there is debt is retired instead of returned. - Raising it repays debt first, then adds permits. All of this, and admission itself, settles against the debt under one lock, so a newer limit always supersedes an older one; there is no background resize task to race. Tokio hands a released permit to a queued waiter before it runs; if that waiter is then cancelled, tokio returns the permit to the semaphore directly, so admission retires any permit it gets while debt is outstanding. The AUTH cap uses the same type. **AUTH shedding.** The permit is taken after signature verification, so only real logins queue for the database checks. The wait is bounded at 2 s. A shed login gets a bare close with no `OK`, exactly like an auth timeout. I chose this from what each client does on main today: - **`OK false`:** the desktop app retries on an unjittered 60 s ladder. Mobile parks the session until the app resumes. The agent harness fails at startup. - **Bare close:** the desktop app, mobile, the agent harness and the CLI all reconnect on their jittered, capped backoff. On the relay, the shed is visible as access line `denied/concurrency_limit` and as `buzz_auth_outcomes_total{outcome="concurrency_limit"}`. **The feature-flag provider is chosen at startup.** `BUZZ_FEATURE_FLAGS_PROVIDER` takes `static` (the default), `environment` or `launchdarkly`, with `BUZZ_LAUNCHDARKLY_SDK_KEY` and an optional `BUZZ_LAUNCHDARKLY_RELAY_PROXY_URL`: - **Exactly one provider.** The value is parsed into a single provider, so there is no stacking and no fallback, and the evaluator never changes for the life of the process. The variable sits outside the `BUZZ_FEATURE_FLAG_` namespace, so it can never be read as a flag. - **Startup failures.** Local misconfiguration fails startup: an unknown provider, a missing key, or a malformed proxy URL. LaunchDarkly being unreachable, or rejecting the key, does not. The relay starts on declared defaults, the SDK keeps connecting, and `buzz_feature_flags_ready{provider}` shows whether the provider has initialized. A revoked key must not crash-loop every pod over a tuning knob. - **Every build carries the LaunchDarkly adapter.** It stays inert unless selected, so one published image serves every deployment and each deployment's configuration pins its provider. ## Design-doc amendments (`crates/buzz-feature-flags/README.md`) - **Provider selection.** The compile-time "exactly one provider" contract becomes the startup selection above. The only-one guarantee now holds per process instead of per build. - **Startup.** Failing closed now covers local configuration only. - **Guardrails.** "Flags may choose between equivalent implementations only" gains one exception: a flag may set an operational limit as long as it can only tighten it, and shedding under that limit stays retryable. - **Evaluation context.** `EvaluationContext::for_process()` adds a process context, which LaunchDarkly sees as a single `relay`-kind context keyed `relay`. ## What got simpler - **Removed the compile-time provider contract:** the nested fixture crate (with its own 2.8k-line `Cargo.lock`), its 229-line contract test, and the exact-one `compile_error!` guards it required consumers to copy. One startup parse plus a unit test replaces them. - **One gate type serves both caps.** The history-read gate became that generic type instead of gaining a sibling. - **Net change:** about −2.3k lines. ## Review-Proven Rules applied - **#1:** shedding is retryable (`PoolTimedOut` → CLOSED for history, a bare close for AUTH) and never an empty success. Provider failures are logged and surfaced through a gauge. - **#2:** the gate settles every limit change and permit release under one lock. The table test covers lowering while idle, lowering below the count held, lowering then raising before holders leave, lowering then raising past the count held, and restoring the ceiling. - **#3:** the tests drive the production call sites: `Db::get_channel_window_with_session` under a lowered and raised cap (Postgres), and `handle_auth` under a full AUTH gate. The controller test runs the real refresh loop. - **#4:** values are clamped, waits are bounded, and the refresh runs on a fixed interval with skipped missed ticks. - **#9:** AUTH shedding sends clients down their jittered reconnect backoff rather than an unjittered or terminal path (see above). ## Review rework (`92a975c8`, `3f09cb33`) - **Keys renamed** to `buzz.relay.concurrency.*`: LaunchDarkly keys cannot be renamed later, and the flag project may be shared with other services. - **Shutdown:** LaunchDarkly's final flush has no deadline of its own, so `close()` now runs it on a detached thread and waits at most 2 s, after the audit drain. (The blocking pool would not bound it: the runtime waits for blocking tasks when it drops.) - **Refresh loop:** a panicking refresh is caught, logged and counted (`buzz_concurrency_limit_refresh_panics_total`), and the next tick retries, so the caps cannot freeze silently. - **Runbook:** `docs/operator-concurrency-caps.md`, covering how to tighten a cap and the cases in which a tightened cap returns to its ceiling (mistyped or deleted flag, pod started during a provider outage). - **AUTH test:** asserts the second login passes the gate and returns its permit; the gate permit is renamed so it no longer shares a name with the NIP-FI effect permit. Gated on a Linux workstation at `92a975c8` (`3f09cb33` is rustfmt only; `cargo fmt --check` passes there): clippy `-D warnings`, `buzz-feature-flags` (50), `buzz-relay --lib --bins` (1740), the new modules under parallel `cargo test`, and the Postgres AUTH tests (27). Each new test fails under its mutant: the refresh without `catch_unwind`, `close()` with no deadline (hangs until killed), and the AUTH handler with the gate skipped. ## Testing Re-run in full at `3c289134` after review rework, on a Linux workstation: - `cargo fmt --check`; `cargo clippy -p buzz-feature-flags -p buzz-db -p buzz-relay --all-targets --all-features -D warnings`. - `buzz-feature-flags` with and without `launchdarkly` (50 and 24 tests), `buzz-db --lib` (209), and `buzz-relay --lib --bins` (1738), plus a plain parallel `cargo test` of the new relay modules. - Postgres: the `buzz-db` routed-read and history-gate tests (55), and the `buzz-relay` AUTH and connection tests (38). - `a_cancelled_waiter_cannot_reopen_a_lowered_gate` fails with the debt check in `admit` removed. - The new infra-free relay tests are added to the CI unit lane (`Justfile` and `scripts/run-tests.sh`). - E2E against a local relay at `06c39d64`: cap gauges, sequential logins and history under cap 1, a 20-login burst (19 shed with a bare close, all reconnected), and LaunchDarkly misconfiguration and unreachability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
sprout.svgfavicon and remove the oldsprout.pngreferenceTesting
desktop-check,desktop-build,desktop-tauri-check,rust-fmt,rust-clippy,rust-tests)