mirror of
https://github.com/block/buzz.git
synced 2026-11-07 09:52:03 +00:00
The renderer owned the whole archive path: it listed saved subscriptions, opened a live REQ per scope, buffered inbound frames, batched them, and handed each batch back over IPC to be written to SQLite. Every archived frame therefore made a round trip into JS for no reason other than history — nothing in that path is a rendering concern. `archive::sync` now runs the whole pipeline natively. It subscribes from the saved subscriptions, buffers, flushes on the same thresholds the JS manager used (FLUSH_BATCH_SIZE 25 / 2000ms deadline — parity, not a retune), archives via SQLite directly, and emits `archive-agent-metrics-changed` when a batch actually persisted metric rows. `archiveSyncManager.ts` is deleted. `native_relay_client` is the shared piece underneath: one authenticated socket per (relay, pubkey), multiplexed subscriptions, declarative `set_subscriptions` reconciliation, and exponential reconnect backoff. A 30s read timeout is idle, not failure, and is discriminated by error variant rather than message text so a reworded error cannot turn every quiet period into a reconnect storm. A relay CLOSED drives its own recovery: per-id retry state lives next to `open` in the connection (not in `desired`, which is reloaded from SQLite and would resurrect a deletion), a dedicated select! deadline arm fires the reopen and is disabled when nothing is scheduled, and the CLOSED message is classified terminal / rate-limited / retryable with the same prefixes the renderer used, `auth-required:` deliberately retryable. Rate-limited arms the shared relay_admission gate and waits max(backoff, hint); backoff is 1s→30s saturating; attempts reset on EVENT or EOSE. Terminal suppression is per-socket by design: a reconnect retries a terminal id once through the normal path, because relay policy can change and one REQ per reconnect is bounded. The sync lifecycle is owned, not raced. The renderer allocates a monotonic lease synchronously in effect order — intent order, which IPC completion order is not — and Rust ignores any start/stop older than the highest (epoch, lease) mark it has seen; a stop advances the mark, so a delayed start cannot resurrect a stopped task. Above the lease, Rust mints a realm epoch, published atomically with minting under the same lock that orders lifecycle calls: announcing IS what supersedes the previous realm, so a separately-held counter would leave a window in which a dead realm's delayed calls still win. The renderer awaits the epoch before its first lifecycle command. Ownership is main-window-only via the established huddleWindowChannelId() exclusion: a huddle companion mounts the same tree in a concurrent realm, and concurrent owners cannot be ordered by any newest-wins clock. What stays in JS is the start gate, deliberately. Kind 24200 is relay-ephemeral, so frames emitted before the listener opens are lost permanently, and only the renderer knows when observer reconciliation finished seeding 24200 into the saved subscription. The backend task is therefore not self-starting: `useArchiveSync` starts it once reconciliation resolves and stops it on unmount. `RelaySession.revision` was documented as rejecting stale in-flight reconciliation but never did — removed rather than repaired: declarative reconciliation re-reads the desired set every pass, so for the open set there is no generation to guard. That argument does not extend to CLOSED retry state, whose validity depends on the id having been continuously desired — history that coalesced wakes erase. So departures are recorded at write time: set_subscriptions diffs old against new desired under the one SessionState lock and reconcile snapshots the desired set and drains those departures in a single acquisition, pruning the retry entries they invalidate. Without this, deleting and recreating the same saved subscription (byte-identical id by construction) inherited the old terminal latch and was suppressed for the life of the socket. Two stale-frame races are closed alongside: a CLOSED for an id not in `open` is stale and mints nothing (our own CLOSE raced it, same defense the EVENT arm already had), and an EOSE for an id not in `open` wakes a reconcile — EOSE is the only ordered fence on the wire, and without that wake a stale terminal CLOSED against a recreated id blackholes a live subscription with no timer or wake left to recover it. A subscription id's filter is immutable for the life of a session (a CLOSED carries only the id, so a rejection of the old filter is indistinguishable from one of the new); the write-time diff detects violations and logs them, with post-violation behavior deliberately unspecified. The retries doc carries the full eviction table, including the deliberately omitted absent-from-snapshot prune and the inductive argument for why it is unreachable. Tests: archive/sync_tests.rs drives the real run_sync body through a fake IO seam (filter parity, flush thresholds, failure isolation) plus the ownership contract (out-of-order start/stop both directions, announcement supersedes a dead realm's delayed calls before any new lifecycle call, publish-(epoch,0) does not lock out the announcing realm). native_relay_client's stub-relay test completes the NIP-42 handshake over a real TCP socket, injects CLOSED with the desired set unchanged, and proves the REQ is retried by the deadline arm; its lifecycle suite (split into native_relay_client_tests.rs to stay under the file-size ratchet) pins the retry-eviction contract with six mutation-controlled tests, including the coalesced delete-recreate that discriminates write-time recording from any observe-time prune, and the stale-CLOSED/EOSE-heal schedules — the relay-backed #[ignore] test additionally proves the REQ shape against a real relay. useArchiveSync.test.mjs owns the start gate, realm ownership, and post-reload realm supersession via fresh module instances. observer-archive-policy.spec.ts owns wiring, with payload receipts that announce precedes start and start carries a numeric epoch. Every claim was mutation-checked; the vacuous first drafts (wake-masked reopen, policy re-implementation, precondition-rebuilding supersession, same-scope no-op escape) were each caught by their mutants and rewritten. Includes one move-only hunk that is not archive work: the push-to-talk global-shortcut handler moves from lib.rs into `ptt_shortcut::install`, mirroring the existing `app_menu::install` seam, paying the 1000-line ratchet budget in the module that owns the registration lifecycle. The handler body is proven token-identical with a mutated-body negative control. lib.rs is 917 lines. Co-authored-by: Tyler Longwell <tlongwell@squareup.com> Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
159 lines
8.0 KiB
TOML
159 lines
8.0 KiB
TOML
[workspace]
|
|
# Explicit: membership must NOT be inferred from the path-dependency edge below.
|
|
# With a bare `[workspace]` and no `members`, `cargo test/check --workspace`
|
|
# expands to a set that excludes this crate, and its gates pass green-and-empty
|
|
# over a real defect. Verified: Sami Arm A/D, Dawn `.scratch/armA`.
|
|
members = ["crates/buzz-terminal"]
|
|
|
|
[package]
|
|
name = "buzz-desktop"
|
|
version = "0.5.14"
|
|
description = "Buzz desktop app"
|
|
authors = ["you"]
|
|
edition = "2021"
|
|
|
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
|
|
[lib]
|
|
# The `_lib` suffix may seem redundant but it is necessary
|
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
|
name = "buzz_lib"
|
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
|
|
|
[features]
|
|
default = ["system-keyring"]
|
|
mesh-llm = ["dep:iroh", "dep:mesh-llm-sdk", "dep:mesh-llm-host-runtime", "dep:mesh-llm-client", "dep:mesh-llm-node", "dep:mesh-llm-system", "dep:mesh-llm-events"]
|
|
# OS keyring backing for desktop secret storage (nsec private keys). When
|
|
# disabled, secrets fall back to 0o600 files. On by default for real builds.
|
|
system-keyring = ["dep:keyring"]
|
|
|
|
[build-dependencies]
|
|
base64 = "0.22"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
tauri-build = { version = "2", features = [] }
|
|
|
|
[target.'cfg(unix)'.dependencies]
|
|
libc = "0.2"
|
|
ctrlc = { version = "3", features = ["termination"] }
|
|
|
|
[target.'cfg(target_os = "linux")'.dependencies]
|
|
keyring = { version = "3.6.3", default-features = false, features = ["sync-secret-service", "vendored"], optional = true }
|
|
# Used directly (alongside tauri-plugin-notification) so we can hold the posting
|
|
# D-Bus connection open. GNOME 46+ dismisses a notification the moment that
|
|
# connection is dropped, which the plugin does immediately. Default features
|
|
# keep the pure-Rust zbus backend, matching the plugin (no libdbus needed).
|
|
notify-rust = "4"
|
|
# Enable getUserMedia in the WebKitGTK webview (see src/linux_media.rs). Pinned
|
|
# to the exact version wry links so both resolve to one webkit2gtk-sys and we
|
|
# don't get duplicate symbols; bump in lockstep with wry.
|
|
webkit2gtk = { version = "=2.0.2", features = ["v2_22"] }
|
|
|
|
[target.'cfg(target_os = "macos")'.dependencies]
|
|
block2 = { version = "0.6", default-features = false, features = ["std"] }
|
|
objc2 = { version = "0.6.4", default-features = false }
|
|
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSEvent", "NSHapticFeedback", "NSMenu", "NSMenuItem", "NSStatusItem", "block2"] }
|
|
objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSDictionary", "NSError", "NSBundle", "NSObject", "NSProcessInfo", "NSString"] }
|
|
objc2-user-notifications = { version = "0.3.2", default-features = false, features = ["block2", "UNNotification", "UNNotificationContent", "UNNotificationRequest", "UNNotificationResponse", "UNNotificationSettings", "UNNotificationTrigger", "UNUserNotificationCenter"] }
|
|
keyring = { version = "3.6.3", default-features = false, features = ["apple-native", "vendored"], optional = true }
|
|
security-framework = { version = "3.7.0", features = ["OSX_10_15"] }
|
|
window-vibrancy = "0.6"
|
|
user-idle = { version = "0.6", default-features = false }
|
|
plist = "1"
|
|
|
|
[target.'cfg(windows)'.dependencies]
|
|
windows-sys = { version = "0.61", features = ["Win32_Security", "Win32_Storage_FileSystem", "Win32_System_JobObjects", "Win32_System_Registry", "Win32_System_Threading", "Win32_Foundation"] }
|
|
keyring = { version = "3.6.3", default-features = false, features = ["windows-native", "vendored"], optional = true }
|
|
user-idle = { version = "0.6", default-features = false }
|
|
|
|
[dependencies]
|
|
atomic-write-file = "0.3"
|
|
anyhow = "1"
|
|
dirs = "6"
|
|
tauri = { version = "2", features = ["macos-private-api", "tray-icon"] }
|
|
tauri-plugin-deep-link = "2"
|
|
tauri-plugin-opener = "2"
|
|
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
|
tauri-plugin-window-state = "2"
|
|
tauri-plugin-dialog = "2"
|
|
tauri-plugin-updater = "2"
|
|
tauri-plugin-process = "2"
|
|
infer = "0.19"
|
|
hex = "0.4"
|
|
ed25519-dalek = "=3.0.0-rc.0"
|
|
tokio = { version = "1", features = ["fs", "sync", "rt", "macros", "time", "net", "io-util"] }
|
|
tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"] }
|
|
tokio-util = { version = "0.7", features = ["rt"] }
|
|
bytes = "1"
|
|
futures-util = "0.3"
|
|
opus = "0.3"
|
|
neteq = { version = "0.8", default-features = false }
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
serde_yaml = "0.9"
|
|
toml = "0.8"
|
|
nostr = { version = "0.44", features = ["nip44", "nip49"] }
|
|
# OS-entropy source for backup passphrase generation (already in the tree as a
|
|
# transitive dependency; pinned here for direct use).
|
|
getrandom = "0.2"
|
|
zeroize = "1"
|
|
percent-encoding = "2"
|
|
reqwest = { version = "0.13", features = ["json", "query", "stream", "blocking"] }
|
|
rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "std"] }
|
|
url = "2"
|
|
buzz_core_pkg = { package = "buzz-core", path = "../../crates/buzz-core" }
|
|
buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona" }
|
|
buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" }
|
|
buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" }
|
|
buzz_voice_pkg = { package = "buzz-voice", path = "../../crates/buzz-voice" }
|
|
buzz_terminal = { package = "buzz-terminal", path = "crates/buzz-terminal" }
|
|
buzz_ws_client_pkg = { package = "buzz-ws-client", path = "../../crates/buzz-ws-client" }
|
|
portable-pty = "0.9"
|
|
iroh = { version = "1.0.2", optional = true }
|
|
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true }
|
|
mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
|
|
# Model catalog + hardware survey for the Share-compute model picker (same
|
|
# diagnose pattern as mesh-console). Lib name of mesh-llm-client is mesh_client.
|
|
mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-client", optional = true }
|
|
mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-node", optional = true }
|
|
mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-system", optional = true }
|
|
mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-events", optional = true }
|
|
base64 = "0.22"
|
|
sha2 = "0.11"
|
|
tar = "0.4"
|
|
bzip2 = "0.6"
|
|
chrono = { version = "0.4", features = ["serde"] }
|
|
tauri-plugin-global-shortcut = "2"
|
|
tauri-plugin-notification = "2.3.3"
|
|
uuid = { version = "1", features = ["v4", "v5"] }
|
|
png = "0.18"
|
|
# wayland-data-control: without it arboard is X11-only on Linux, so copies made
|
|
# in a Wayland session land in XWayland's clipboard where Wayland-native apps
|
|
# never see them (set_text still returns Ok). The backing wl-clipboard-rs dep is
|
|
# target-gated inside arboard; at runtime X11 sessions still fall back to X11.
|
|
arboard = { version = "3", features = ["wayland-data-control"] }
|
|
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp", "gif"] }
|
|
zip = "8"
|
|
flate2 = "1"
|
|
sherpa-onnx = "1.12"
|
|
regex = "1"
|
|
rusqlite = { version = "0.37", features = ["bundled"] }
|
|
axum = "0.8"
|
|
rodio = "0.22"
|
|
earshot = "1.0"
|
|
rubato = "3.0"
|
|
audioadapter-buffers = "3.0"
|
|
tempfile = "3"
|
|
strip-ansi-escapes = "0.2"
|
|
tracing = "0.1"
|
|
|
|
[dev-dependencies]
|
|
tauri-utils = "2"
|
|
# `test-util` enables tokio's paused-clock (`start_paused`) so the relay
|
|
# admission gate tests can assert exact wait durations without real sleeps.
|
|
tokio = { version = "1", features = ["test-util"] }
|
|
# The relay's media validation, so the snapshot-sharing tests can prove the
|
|
# full export → sanitize → relay-accept → import contract end to end.
|
|
buzz_media_pkg = { package = "buzz-media", path = "../../crates/buzz-media" }
|