The §2.8 canonical-linkage freeze path retained the inbound event as the
head before attempting the corrective library-authoritative re-retain, then
swallowed a failed re-retain to stderr and returned Ok. That left the
non-authoritative head retained with no recovery: replay is dead because the
same event re-arriving is Skipped at the equal-created_at guard before the
convergence branch runs.
Propagate the corrective re-retain failure (converge_frozen_linkage) so the
command cannot report success over a divergent head. The durable retry owner
is the boot-time reconcile_agents_to_events pass, which re-diffs the
still-authoritative on-disk record against the retained head every launch and
re-queues the corrective row at a monotonic bump. Surface the freeze reason
as a typed InboundReconcileOutcome (reconcile_inbound_persona_event now
returns it) instead of stderr-only, and consume it in usePersonaSync.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phase 2 (revised) of the cross-workspace agent library: the read side of
the definition-instance relation resolver and the inbound kind:30177
canonical-linkage rule, with the interim fail-closed new-link posture. No
instance-removal/insertion coordinator (that is Phase 4b).
Relation resolver: MutationRoute::for_linked_definition resolves an
instance's persona_id against the raw keyless definition store to classify
its linkage (persona_id IS the linked definition's slug). This is the one
canonical join every library mechanism uses; nothing re-derives the
persona_id join ad hoc.
Inbound 30177 canonical linkage: apply_inbound_managed_agent now consults
the resolver and freezes two linkage-authorship cases, applying only safe
per-instance fields (name, parallelism, respond_to, respond_to_allowlist):
- OwnedByLibrary: the matched instance is linked to a projected definition
and the event would clear or re-point persona_id.
- InadmissibleNewLink: the event would newly link a plain instance to a
projected definition (only the Phase-4b coordinator may admit).
A frozen linkage re-retains the local record at a monotonically newer
created_at (via retain_agent_record) so the relay head converges back to
the library-authoritative linkage, mirroring the 30175 rule. Plain-to-plain
relinks and no-op events apply exactly as at head. The round-2 projected
persona preflights stay intact in front of this.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
F1: canonical identity indexing — binding validation parses every owner
and agent pubkey through a canonical-encoding gate (64 lowercase hex +
curve-point check, mirroring parse_canonical_pubkey), rejecting any
non-canonical spelling into the quarantine ladder. This makes the
document-wide raw-string identity index sound: a cross-owner alias can no
longer evade the check by re-casing its hex.
F3: command-boundary preflight — raw_record_by_slug and MutationRoute
gain live consumers. delete_persona and snapshot/team import route on
for_slug; both inbound arms route on for_persona_d_tag (their real match
key, derived from source_team_persona_slug). Each consults the RAW keyless
store BEFORE any destructive effect, so a library-projected target fails
closed with zero side effects until §3 wires the state machine. The merge
seam now compares a SharedSlotFingerprint over the SharedDefinition
allowlist instead of the whole record, so scope-local edits (is_active,
env_vars, timestamps) on a projected record ride the plain path.
F4: the legacy byte-compat test drives the real store save->load->save
path and asserts a byte-exact fixpoint against the pinned-head baseline,
plus key-absence — the assertion now matches its name.
F5: deferred_archives is private; upsert_deferred_archive and
deferred_archive_obligations are the only access outside the module.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Interim review of Phases 0+1 found one CRITICAL and three IMPORTANT
defects plus one MINOR; all are resolved here so Phase 2 can resume.
F1 (CRITICAL): cross-owner binding aliasing. The document identity
index now maps each bound agent_pubkey to its owner and group-quarantines
every entry when one pubkey is bound under two owners; same-owner reuse
across entries stays healthy (§2.5). Per-entry validation could not see
this document-wide alias of a process-global keyring identity.
F2 (IMPORTANT): deploy-intent routing is now validated by
validate_provider_config on read (fail -> Unreadable) and at the
save_deploy_intents writer boundary, so a malformed or secret-bearing
row is never exposed as authoritative routing (§2.1).
F3 (IMPORTANT): the Phase-0 routing seam is present. A raw-record-by-slug
lookup plus a typed MutationRoute decision route delete/inbound/import;
merge_preserving_definitions fails closed when a plain save would delete
or edit the shared slots of a library-projected record, while an
unchanged projected re-pass rides through intact (§2.7).
F4 (IMPORTANT): the P14-I2 provenance regressions are added — legacy
byte-compat round-trip, a concurrent deploy-success pair-churn rollback,
and a non-None deploy stamp surviving apply_definition_view/into_agent_record.
F5 (MINOR): deferred_archives gains encapsulated upsert_deferred_archive
(SET semantics on (scope_id, agent_pubkey)) and a deferred_archive_obligations
read view that collapses legacy duplicate rows to one obligation (§2.3).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The Phase 0 seam made every persona writer merge-preserving; Phase 1 lands
the data model that seam was built for: `library.json` and the scope-local
crash journals, plus their quarantine-preserving IO. No operation mutates
them yet — share, edit, materialize, delete, and deploy land in Phases 2-5.
`library.rs` models the versioned document envelope, `SharedDefinition`
(the allowlisted content a share may carry — credentials, identity,
`env_vars`, activation, and projection metadata are structurally absent),
`LibraryEntry` with its per-scope `ProjectionState` machine, verified
`IdentityBinding`s, and the permanent `DeferredArchive`/`RemovalManifest`
retirement markers. `load_library` classifies per §2.1: an absent file is a
valid empty library, whole-document corruption is preserved as `.invalid`
and blocks all mutation, an unknown version is read-only fail, and a single
malformed / semantically invalid / identity-colliding entry is quarantined
raw while healthy siblings stay usable. Identity collisions on `library_id`,
a live origin, or a non-terminal `(scope, slug)` claim group-quarantine every
collider — never first-wins.
`library/journals.rs` adds the scope-local `pending-agent-keys.json` and
`deploy-intents.json`, which share the owning workspace's failure domain
rather than the library's so a broken `library.json` never blocks a
workspace-local create or deploy. Their failure domains are asymmetric: a
pending-keys failure degrades only that scope's create/import path, while a
deploy-intents failure — unknown version, syntax error, or a duplicate-pubkey
mutex violation validated on read — fails the scope's destructive and deploy
paths closed.
`apply_shared_definition` is the sole writer of shared content onto a scoped
keyless record, assigning exactly the shared slots plus revision/timestamp and
mirroring `into_agent_record` so a populated record is byte-identical to a
freshly projected one. `ManagedAgentRecord` gains
`last_completed_deploy_attempt_id`, the deploy-provenance stamp that forms an
inseparable pair with `backend_agent_id` in `copy_runtime_state`.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The applied-but-blocked test additions in the branch-tip commit left three assertions over the line-width limit, which fails desktop-tauri-fmt-check. Reformatting them clears Rust Lint for subsequent phase pushes.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phase 0 of the cross-workspace agent library (spec §2.7) — the compatibility
seam that lets §3's library machinery land without a data-loss regression.
No library code exists yet; this is the pure refactor that makes every persona
writer merge-preserving by construction.
- `ManagedAgentRecord` gains `library_ref` / `library_applied_revision`
(§2.6), both `#[serde(default, skip_serializing_if)]` so records without
them stay byte-identical to head. Only §3 authors them.
- `apply_definition_view` writes ONLY the view-carried slots onto a canonical
raw record, so any record-only field survives an unrelated save. At head,
`save_personas` reconstructed every record wholesale through
`into_agent_record`, silently erasing such fields on every other definition.
- `save_personas[_at]` now merges views onto existing raw records by slug via
`merge_preserving_definitions`; a genuinely new persona is still projected
fresh. Loader signatures are untouched (P5-I2).
- `load_persona_views[_at]` + `PersonaView` (flattened wire, skip-none) expose
the library metadata at the list/get command boundary; `list_personas`
returns `PersonaView`. The ~78 non-command readers keep the flat loader.
- Two preservation tests: every writer shape through the in-memory seam, and a
full on-disk save/reload cycle through `_at` plus the read-side exposure.
The `AgentDefinition <-> ManagedAgentRecord` conversions move to a new
`types/conversions.rs` submodule (following the existing `types/` split), which
keeps `types.rs` under the 1000-line file-size cap after the two new fields.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Provider-access reconciliation runs in the post-commit section of
apply_workspace, after relay/keys/scope have committed. The previous
.await? propagated any reconciliation failure as a command Err, causing
the frontend to treat the workspace as unapplied and clear appliedKey —
while the new scope was already active. This contradicted the
applied/degraded contract and the useCommunityInit error-handling
assumption.
Fix: replace .await? with a match that returns WorkspaceApplyResult
with applied: true and blocked: Some(reason) on reconciliation failure.
Dependent post-commit steps (event sync, agent restore) remain
unreached on this path, preserving #4053's fail-closed intent. The
frontend parks on the loading gate with a truthful error and the same
retry-by-reapply semantics as the catch block.
Changes:
- scope.rs: add blocked: Option<String> field to WorkspaceApplyResult,
add applied_but_blocked() constructor, document the three states
- workspace.rs: replace .await? with match; return applied_but_blocked
on reconciliation failure; update command doc comment
- useCommunityInit.ts: handle blocked after the !applied branch; update
catch-block comment (reconcile failures no longer arrive as Err)
- tauri.ts: add blocked?: string | null to ApplyWorkspaceResult type
- e2eBridge.ts: add blocked: null to apply_workspace mock result
- runtime_commands_tests.rs: add three new tests covering the new state
and the three-state distinction
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
**Category:** new-feature
**User Impact:** People who lose a desktop identity can securely restore
it from a signed-in Buzz phone without creating a replacement identity.
**Problem:** A fresh or identity-lost desktop could not recover its
existing full Buzz identity from an already-authorized phone.
**Solution:** Add a SAS-confirmed reverse NIP-AB transfer, durable
desktop import, a dedicated mobile recovery entry point, and clearer
desktop recovery dialogs with tested loading, drag-and-drop, and failure
states.
https://github.com/user-attachments/assets/e9215c9c-80d0-462f-9161-0fa184ca2f74
<details>
<summary>File changes</summary>
**crates/buzz-core/src/pairing/session.rs**
Adds the reverse encrypted payload and source-completion state
transitions used for phone-to-desktop recovery.
**desktop/src-tauri/src/commands/identity.rs**
Exposes the existing guarded identity commit path for recovery imports.
**desktop/src-tauri/src/commands/pairing.rs**
Adds recovery-mode pairing, durable nsec import, start serialization,
stale-task protection, and explicit rejection of unsupported recovery
payloads.
**desktop/src-tauri/src/lib.rs**
Registers the recovery pairing command.
**desktop/src/app/App.tsx**
Refreshes the recovered identity before continuing onboarding.
**desktop/src/features/onboarding/machineOnboarding.ts**
Adds recovery transitions to the onboarding state machine.
**desktop/src/features/onboarding/ui/BackupPasswordTimeline.tsx**
Adds the visual backup-to-password-to-unlock progression.
**desktop/src/features/onboarding/ui/IdentityRecoveryPairing.tsx**
Implements QR generation, copy fallback, SAS confirmation, cancellation,
expiry, and completion UI.
**desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx**
Connects private-key, phone, and backup recovery paths to the onboarding
flow.
**desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx**
Polishes recovery dialogs, backup drag-and-drop, loading stability, and
security copy.
**desktop/src/shared/api/tauri.ts**
Keeps the existing pairing API surface focused on standard
desktop-to-mobile pairing.
**desktop/src/shared/api/tauriPairing.ts**
Adds the recovery pairing invoke without growing the ratcheted shared
API file.
**desktop/src/testing/e2eBridge.ts**
Mocks recovery pairing commands and lifecycle events for browser tests.
**desktop/tests/e2e/identity-lost.spec.ts**
Covers lost-identity entry, QR/copy recovery, SAS, cancellation, expiry,
success, errors, backup import, drag-and-drop, and screenshots.
**desktop/tests/e2e/onboarding.spec.ts**
Verifies recovered identities continue through harness setup without
replacement-key side effects.
**mobile/lib/features/pairing/pairing_page.dart**
Adds recovery-only scanning and explicit identity-handoff warnings.
**mobile/lib/features/pairing/pairing_provider.dart**
Recognizes recovery codes, returns the signed-in nsec after mutual SAS
approval, and waits for desktop completion.
**mobile/lib/features/settings/settings_page.dart**
Accepts the recovery route builder at the app composition boundary to
preserve feature isolation.
**mobile/lib/features/settings/settings_page/connection_section.dart**
Adds the signed-in “Send identity to desktop” settings action.
**mobile/test/features/pairing/pairing_page_test.dart**
Covers recovery-only validation and handoff messaging.
**mobile/test/features/pairing/pairing_provider_test.dart**
Covers reverse payload encryption, confirmation ordering, success,
failure, timeout, and cleanup.
</details>
## Reproduction steps
1. Launch Buzz Desktop with identity-lost state and choose **Recover
from your phone**.
2. Confirm the QR and persistent **Copy pairing code** fallback appear
without layout shift.
3. On a signed-in phone, open **Settings → Send identity to desktop**,
scan or paste the recovery code, and compare the six-digit SAS on both
devices.
4. Confirm on both sides and verify Desktop restores the identity and
continues to harness setup.
5. Repeat from identity-lost state with **Recover from a backup file**;
verify picker and drag-and-drop both advance to password entry and
restore the encrypted backup.
6. Exercise cancellation, mismatched/unsupported codes, expired
sessions, and an invalid backup; verify each returns actionable,
non-stuck UI.
## Screenshots
### Desktop phone recovery — complete flow
| Recovery entry | Pairing QR | Code match | Receiving identity |
|---|---|---|---|
| 
| 
| 
| 
|
### iOS Simulator — complete handoff flow
| Settings entry | Recovery scanner | Manual recovery code | Code
confirmation |
|---|---|---|---|
| 
| 
| 
| 
|
### Encrypted backup recovery — adjusted file flow
| File picker | Drag-and-drop target | Password step |
|---|---|---|
| 
| 
| 
|
## Verification
- `cargo test -p buzz-core pairing` — 71 passed
- `just mobile-test` — 1,169 passed
- `pnpm build:e2e && pnpm exec playwright test identity-lost.spec.ts
--project=smoke` — 15 passed
- Full pre-push gates — desktop checks, desktop unit tests, Rust tests,
Tauri checks, and mobile tests passed
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
## Summary
- require the macOS process to be running from an actual `.app` bundle
before initializing `UNUserNotificationCenter`
- keep the existing bundle-identifier requirement
- cover packaged, case-insensitive `.app`, raw `target/debug`, and
extensionless paths
## Why
PR #4799 guarded native notification initialization with
`NSBundle.mainBundle.bundleIdentifier != nil`. Tauri embeds a bundle
identifier in raw development executables, so `tauri dev` passed that
guard and `UNUserNotificationCenter.current()` raised an uncaught
`NSInternalInconsistencyException` because LaunchServices had no bundle
proxy.
## Validation
- focused macOS notification tests: 6 passed
- direct raw debug executable no longer raises the notification-center
exception
- pre-commit formatting hook passed
- pre-push package checks passed on pushed commit
`f29a6664d2a863e7b8aa527f6149fd00b183e4de`
The first push attempt hit an unrelated timing-test failure in
`relay_admission::tests::concurrent_429_extends_the_window_for_parked_waiters`;
its focused rerun passed, and the complete pre-push package suite passed
on the next push.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This change enables a Tauri content security policy that limits
executable content to the packaged application and does not allow inline
scripts.
Relay, media, asset, and Tauri IPC schemes remain available for desktop
compatibility. The policy contains the impact of a future renderer
injection; it does not itself remove an injection bug.
## Testing
- `git diff --check origin/main...codex/security-desktop-csp`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending
Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`
---------
Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The guard is invoked via the fully-qualified path
crate::managed_agents::scope::SCOPE_GENERATION_TEST_LOCK at :146,
matching the style in app_state_scope_tests.rs. The use-block import
added in b450d4693 is therefore unused and triggers clippy -D warnings.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
* origin/main: (24 commits)
fix(reactions): support max-length custom emoji (#3833)
feat(desktop): allow leaving your final community (#3621)
fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
docs(persona-pack): fix stale desktop import instructions (#4500)
fix(desktop): route macos notification clicks (#4799)
feat(mobile): sync themes per community (#3767)
feat(desktop): sync themes per community (#3653)
feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805)
Fix mobile message timeline bounce (#4862)
Polish mobile bottom sheets and profile cards (#4911)
Fix media attachment actions (#4849)
fix(desktop): remove join API token control (#4897)
fix(desktop): allow shared agent mentions (#4913)
Polish mobile top navigation (#4778)
fix(release): tag immutable desktop candidates (#4811)
fix(channels): restrict private-channel invitations (#4612)
fix(acp): reject unattended permission requests (#4609)
fix(workflow): bind trigger author to the signed event (#4607)
fix(git): revoke access for banned relay members (#4608)
...
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
# Conflicts:
# desktop/src-tauri/src/managed_agents/runtime.rs
**Category:** improvement
**User Impact:** People can leave their final Buzz community and return
to **Join or create a community** without losing their signed-in
identity.
**Problem:** Buzz Desktop blocked people from leaving when only one
community remained. Its existing remove action also changed local
configuration without ending relay membership.
**Solution:** Allow the final community to be left. Buzz now asks the
relay to end membership, removes the community locally only after
acceptance, and returns the person to the community selector while
keeping their identity signed in. If other communities remain, Buzz
switches to one of them. Relay rejection or timeout keeps the community
in place and shows an actionable retry error.
<details>
<summary>File changes</summary>
**desktop/src/features/communities/leaveCommunity.ts**
Adds signed kind 28936 publishing for active and inactive community
relays with actionable timeout handling.
**desktop/src/features/communities/leaveCommunity.test.mjs**
Covers event shape, relay selection, acceptance gating, rejection,
timeout messaging, and cleanup.
**desktop/src/features/communities/useCommunities.tsx**
Allows final-community removal and clears community-specific storage
without touching identity.
**desktop/src/features/communities/resolveCommunityRemoval.test.mjs**
Covers final, active, and inactive community removal state transitions.
**desktop/src/app/useCommunityNavigationTransitions.ts**
Gates local removal on relay acceptance and routes to a fallback
community or setup selector.
**desktop/src/app/AppShell.tsx**
Passes the asynchronous leave operation through shell entry points.
**desktop/src/features/communities/ui/EditCommunityDialog.tsx**
Replaces the local-only remove action with a pending-aware Leave
Community action that retains actionable errors.
**desktop/src/features/communities/ui/CommunitySwitcher.tsx**
Enables leaving the final community and carries the asynchronous
callback.
**desktop/src/features/sidebar/ui/AppSidebar.tsx**
Carries the asynchronous leave callback through sidebar props.
**desktop/src/features/sidebar/ui/CommunityRail.tsx**
Enables leaving the final community from rail settings.
**desktop/src/features/sidebar/ui/SidebarProfileCard.tsx**
Carries the asynchronous leave callback through profile community
settings.
**desktop/src/testing/e2eBridge.ts**
Teaches the mock relay to accept NIP-43 leave events.
**desktop/tests/e2e/community-rail.spec.ts**
Updates leave interactions and verifies final-community setup
navigation, storage cleanup, and identity preservation.
</details>
### Reproduction steps
1. Run Buzz Desktop with a signed-in identity and one joined community.
2. Open Community settings and choose **Leave Community**.
3. Confirm the app shows **Join or create a community** and the existing
identity remains signed in.
4. Repeat with two communities and confirm leaving the active one
switches cleanly to the remaining community.
5. Reject or withhold the relay `OK` response and confirm the community
remains configured with an actionable error in the dialog.
### Test plan
- `pnpm check`
- `pnpm build`
- `pnpm test` (3,913 passing)
- `pnpm build:e2e && pnpm exec playwright test
tests/e2e/community-rail.spec.ts --grep "final community"`
<img width="557" height="316" alt="image"
src="https://github.com/user-attachments/assets/b628182f-cba5-451d-ae4b-bee8d8dd19aa"
/>
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Replace timing-based concurrency tests with compile-enforced structural
guard ownership proofs per Thufir's binding shape consult.
Item 1 (generation lock): add SCOPE_GENERATION_TEST_LOCK to
test_fallback_relay_never_claims_during_identity_import and
test_scope_generation_guard_rejects_stale_scope_for_import; exhaustive
indirect-mutator sweep confirmed all remaining callers guarded.
Item 2 (writer test): widen on_after_restore to
FnOnce(&mut Vec<ManagedAgentRecord>, &MutexGuard<'_, ()>) — callback
borrows the actual store guard. Dropping or removing the guard before
the call is a compile error. Writer completes one full transaction
(lock → load → WRITER_EDIT → save → writer_committed) after records_loaded
releases it; on_after_restore saves COMP_SENTINEL under the live borrow.
Delete writer_at_store_lock pre-lock signal and all timing-based comments.
Item 3 (contender test): widen on_transition_acquired to
FnOnce(&MutexGuard<'_, ()>) in both start_pair_lazy_for_with_hook and
start_pair_for_with_hook — callback borrows the actual transition guard.
Drop or removal before the call is a compile error. Remove AtomicBool,
try_recv, not-fired-during assertion, and all ns-vs-ms timing rationale.
Proof by mutex exclusion: on_transition_acquired cannot execute during
compensation because both borrow the same mutex.
Production delegates unchanged: compensate_drain passes |_, _| {},
start_pair_lazy_for and start_pair_for pass |_| {} for on_transition_acquired.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- deliver macOS notifications through `UNUserNotificationCenter`
- route notification clicks to the referenced channel or thread through
the existing frontend activation path
- preserve click targets across cold startup and frontend remounts with
a small process-wide activation queue
- keep Linux notification activation behavior unchanged
## Architecture
A single `UNUserNotificationCenterDelegate` is installed during Tauri
setup. Each notification stores its navigation target in `userInfo`. On
click, Rust queues the target before emitting a wake-up event; the
frontend atomically drains the queue and dispatches the existing
notification action. The queue is the source of truth, which prevents
cold-start loss and duplicate delivery.
## Validation
Verified at `a81241611d617becf7640bee6fe56b5cdb4d0fab`:
- Biome format/check and lint
- TypeScript typecheck
- repository and desktop-Tauri `cargo fmt --check`
- repository and desktop-Tauri Clippy with `-D warnings`
- full pre-push desktop tests and Tauri workspace checks/tests
- desktop production build
Manual macOS validation passed: after explicitly ad-hoc signing the
local bundle with `xyz.block.buzz.app`, the operator confirmed real
Notification Center delivery and click navigation.
<details>
<summary>Local macOS test procedure</summary>
Tauri's generated ad-hoc signing identifier is not accepted by
`UNUserNotificationCenter`. Re-sign the local bundle with its bundle
identifier and keep other Buzz copies closed:
```bash
just desktop-release-build
APP="$HOME/.cache/cargo-target/aarch64-apple-darwin/release/bundle/macos/Buzz.app"
codesign --force --deep --sign - \
--identifier xyz.block.buzz.app \
--entitlements desktop/src-tauri/Entitlements.plist \
"$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
pkill -x buzz-desktop || true
open -n "$APP"
```
</details>
Buzz channel: `55e2bfca-1b38-48fb-9dc2-584d400501f3`
---------
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
OpenClaw connects to a single shared Gateway daemon. Spawning the
default 10 ACP workers per agent is both resource-expensive and
architecturally wrong — each worker opens a separate gateway connection.
Tyler's ruling: cap at 5, lower if needed.
## Contract
Store the requested value (1–32) verbatim at every persistence and wire
boundary. Apply `effective = min(requested, harness_cap)` only at the
four enforcement points:
| Boundary | Implementation |
|---|---|
| Local spawn | `BUZZ_ACP_AGENTS` env var in child `Command` |
| Remote deploy | `launch.policy_env["BUZZ_ACP_AGENTS"]` + legacy
`parallelism` field |
| Restart badge | `SpawnConfigSnapshot.parallelism` stores effective
value; the diff surface displays what actually runs |
| UI copy | Amber hint when requested > cap; no `max` attribute, no
save-path clamp |
`BUZZ_ACP_AGENTS` is added to `RESERVED_ENV_KEYS` — the Desktop resolves
the effective value into `policy_env`; a user-supplied override in `env`
would bypass the cap and is silently stripped.
## Changes
**`managed_agents/parallelism.rs`** (new) — policy core:
- `OPENCLAW_MAX_PARALLELISM = 5`
- `harness_max_parallelism(command)` — keyed on
`normalize_command_identity` so path prefixes, `.exe` suffixes, and
other cosmetic differences are ignored
- `effective_parallelism(command, value)` — identity for uncapped
harnesses
- `acp_agents_value(command, parallelism)` — `env("BUZZ_ACP_AGENTS", …)`
helper
**`runtime.rs`** — spawn clamp: `BUZZ_ACP_AGENTS =
acp_agents_value(effective_command, record.parallelism)`
**`agents_deploy.rs`** — deploy egress clamp: `build_deploy_payload`
resolves `effective_parallelism` once from `descriptor.command`; both
`launch.policy_env["BUZZ_ACP_AGENTS"]` and the legacy top-level
`parallelism` field use that value — the two are always consistent
regardless of stale `record.agent_command` pins
**`spawn_snapshot.rs`** — `from_inputs` stores
`effective_parallelism(&descriptor.command, record.parallelism)` in the
`parallelism` field. Over-cap edits that don't change the pool (e.g. 10
→ 8, both clamp to 5 on OpenClaw) produce equal snapshots; cap crossings
(8 → 3) produce different snapshots.
**`AcpRuntimeCatalogEntry.max_parallelism: Option<u32>`** — derived from
the static definition command, not the probed `entry.command` (which may
be `null` for unavailable entries), so unavailable OpenClaw entries
still carry the cap. Propagated through all four catalog constructors
(builtin discovery, preset catalog construction, custom discovery,
custom-save response), IPC types
(`RawAcpRuntimeCatalogEntry.max_parallelism`), and the frontend catalog
type.
**UI** — `EditAgentAdvancedFields` and `PersonaAdvancedFields` show an
amber hint when `selectedRuntime.maxParallelism` is set and the current
value exceeds it. Cap and label come from the catalog entry — no
hardcoded 5 in TS. No `max` attribute on inputs; the input stays
`type="text"` with 1–32 copy.
**Docs** — `docs/remote-agents.md`: `BUZZ_ACP_AGENTS` moved from the
deliberately-non-reserved section to reserved; new contract documented.
`desktop/src/features/agents/AGENTS.md`: command-keyed execution policy
documented as the sanctioned second metadata source feeding the catalog
projection.
## Tests
**Rust** (`parallelism.rs`):
- `policy_table` — `harness_max_parallelism` and `effective_parallelism`
across all openclaw variants and uncapped harnesses
- `acp_agents_value_openclaw_above_cap_is_capped` — spawn-env seam
- `override_direction_*` — both override directions (openclaw runtime +
goose override; goose runtime + openclaw override)
- `summary_persona_inherited_*` — live persona wins over stale
`agent_command`
- `snapshot_export_carries_requested_definition_parallelism` — requested
value travels wire/sync unchanged
**Rust** (`spawn_snapshot/tests.rs`):
- `openclaw_above_cap_parallelism_snapshots_equal` — stored 10 vs 8,
both clamp to 5 → snapshots equal
- `openclaw_cap_crossing_parallelism_snapshots_differ` — 8 (clamps to 5)
vs 3 → snapshots differ
**Rust** (`discovery/presets.rs`):
- `openclaw_preset_unavailable_carries_max_parallelism` /
`openclaw_preset_available_carries_max_parallelism` — catalog metadata
present with `command: null` and with a resolved path
**Rust** (`agents_deploy.rs`):
- `launch_block_openclaw_over_cap_policy_env_is_capped` — direct
`launch.policy_env` seam
-
`deploy_payload_json_stale_goose_record_live_openclaw_descriptor_both_capped`
— stale `record.agent_command=goose`, live descriptor=openclaw: both
fields cap to 5
-
`deploy_payload_json_stale_openclaw_record_live_goose_descriptor_both_uncapped`
— stale `record.agent_command=openclaw`, live descriptor=goose: both
fields pass through requested
- `deploy_payload_json_explicit_openclaw_override_both_capped` —
explicit `agent_command_override=openclaw`: both fields cap to 5
**Rust** (`persona_events/stale_pin_tests.rs`):
- `apply_persona_snapshot_goose_to_custom_harness_drops_stale_goose_pin`
— custom-direction stale-pin drop (builtin pin → loaded custom harness
via `update_loaded_harness_registry`)
**TypeScript** (`agentParallelism.test.mjs`):
- `parallelismCapHint` — at/below cap (null), above cap (hint includes
label and cap value), singular form for cap=1, uncapped harness (null)
**TypeScript** (`tauri.test.mjs`):
- `fromRawAcpRuntimeCatalogEntry` round-trips `max_parallelism` →
`maxParallelism`; absent when `undefined`
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Item 1 — contender test (production-called hook):
Move start_pair_for_with_hook and start_pair_lazy_for_with_hook to
runtime_commands_seams.rs (extracted to stay under the 1000-line gate;
included via #[path]). Production start_pair_for delegates to
start_pair_for_with_hook with no-op hooks; start_pair_lazy_for delegates
through start_pair_lazy_for_with_hook. The test-file mirror
start_pair_for_with_hook is deleted from runtime_commands_tests.rs. The
contender test calls the production-callable seam: removing
managed_agent_runtime_transition from start_pair_for_with_hook would
also remove it from the production delegation chain, making the test a
faithful proxy.
Item 2 — writer test (on_after_restore callback):
Remove the unconditional save_managed_agents_at from the production
compensate_drain_with_hook body (step-7 was a no-op duplicate that also
masked the real compensation result on error). Add on_after_restore hook
invoked after compensate_drain_for returns, still under both guards.
Production compensate_drain passes a no-op; test injects a mutation + save.
The writer test: on_records_loaded signals writer and waits for
writer_at_store_lock_rx before returning; on_after_restore mutates
COMP_SENTINEL in-memory and saves (assert/unwrap). Both COMP_SENTINEL and
WRITER_EDIT must appear on disk. If the store guard is dropped before
on_after_restore, the writer interleaves, loads without COMP_SENTINEL, and
the first assertion fails deterministically.
Item 3 — E2E classification:
thread-focus-mode.spec.ts:139 failed in run 30987602439 (branch head
6739f157e). The prior branch commit 27ea60724 (same TypeScript changes,
different Rust) had green CI including all E2E shards (run 30981177509).
The delta between 27ea60724 and 6739f157e is Rust-only; the spec tests
viewport scroll preservation in focus/split mode — no Rust path. The
failure is a CI flake, not a branch regression.
Item 4 — doc comment alignment:
Updated all compensation and contender test comments to describe only what
the execution establishes. Removed references to step-7, stale failure-mode
descriptions, and start_pair_for in favour of start_pair_for_with_hook.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
User-facing error for missing ACP harness commands has been pointing
released-build users to run `cargo build --release --workspace` and read
TESTING.md — both dead ends for anyone not building from source.
Updated message acknowledges that antivirus software can quarantine
bundled binaries and provides practical remediation steps. Preserves
pointer to TESTING.md for source builds.
Fixes issue context from #4491.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Writer test: change writer to block directly on managed_agents_store_lock
(not via start_pair_lazy_for_with_hook) so the test fails deterministically
if the store guard is dropped before the step-7 save. The hook mutates
records in-memory; the step-7 save writes the mutation to disk while the
lock is still held; the writer loads after the lock is released.
Contender test: add try_recv() check on a dedicated channel inside
on_records_loaded alongside the existing atomic bool. on_transition_acquired
sends to both channels. try_recv() is deterministic: without the transition
lock, on_transition_acquired fires in nanoseconds; by the time on_records_loaded
runs (after file I/O), the channel contains the message. Documents the
time-differential argument explicitly in the test comment.
Full-tail PID: capture child.id() before wrapping into ManagedAgentProcess;
assert exact equality with receipt.pid (assert_eq! not assert! pid > 0).
SCOPE_GENERATION_TEST_LOCK: add to all remaining tests that mutate or
capture scope generation: app_state_scope_tests.rs (4 tests),
global_agent_config_tests.rs (4 tests), runtime_commands_tests.rs (2 tests),
scope.rs (5 generation tests). All tests that advance SCOPE_GENERATION
now participate in the process-global serialization mutex.
Trim runtime_commands.rs doc comments to stay at 1000 lines (gate limit).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
SCOPE_GENERATION_TEST_LOCK is a std::sync::Mutex held across await points
in test_full_tail_stop_spawn_receipt_register_save and
test_relay_mesh_preflight_precedes_stop to prevent concurrent tests from
advancing the generation counter mid-test. The deadlock risk is acceptable
in test-only code: the lock is never held across blocking operations, only
across async coordination inside a single test. Suppress the lint explicitly
with a comment explaining the rationale.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
test_full_tail_stop_spawn_receipt_register_save and
test_relay_mesh_preflight_precedes_stop both capture the scope generation via
current_scope_generation() and call into restart_under_captured_epoch_for or
restart_local_agent_on_config_change_for, which validate the generation under
a lock. Without SCOPE_GENERATION_TEST_LOCK, a concurrent test calling
next_scope_generation() can advance the counter between capture and validation,
causing the epoch to return Skipped instead of the expected outcome.
Both tests now hold SCOPE_GENERATION_TEST_LOCK for the duration of their
execution, matching the serialization pattern used by the workspace-transition
tests.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Four IMPORTANT findings: each assertion now fails if the production guard or
save it claims to prove is removed.
Fix 1 — compensate_drain writer test (genuine store-lock contention):
Add compensate_drain_with_hook seam; on_store_acquired hook fires while BOTH
locks are held. Test writes COMP_SENTINEL under the lock, signals writer,
waits for writer to queue on the store lock, then releases. Final disk state
must contain BOTH COMP_SENTINEL and WRITER_EDIT — fails if the guard is
dropped before compensate_drain_for's save.
Fix 1b — start-contender test (production start-lock seam):
Add start_pair_lazy_for<R: tauri::Runtime> generic seam; production
start_managed_agent_runtime_pair_lazy and start_pair delegate to it.
Contender now calls start_pair_lazy_for (the seam the production adapter
calls) instead of a raw mutex lock — proves compensation blocks production
starts, not just an arbitrary lock acquisition.
Fix 2 — Record-Mesh TOCTOU (async production driver):
Drive restart_local_agent_on_config_change_for (full async driver). Injected
mesh_fn rewrites provider to relay-mesh after the driver has resolved
mesh_model_id=None; epoch re-resolves to Some("auto") != None → TOCTOU guard
fires → Skipped before stop. Removing the in-epoch re-resolve guard would
allow the epoch to proceed, calling stop_fn and failing the assertion.
Fix 3 — Helper-vs-helper serialization (pre-acquisition hook):
Add with_workspace_transition_preflight_with_hook and
install_client_under_workspace_transition_with_hook to mesh_llm_scope.rs.
Both serialization tests use a proper 3-step handshake: holder signals
lock_held BEFORE publishing state; contender fires pre-acquisition hook
signaling at_lock_boundary; only then does the holder publish and release.
Removing the shared lock would let the contender bypass the boundary,
inverting each test's expected outcome.
Fix 4 — Full-tail receipt and disk assertions:
write_receipt_fn captures all receipt fields: key.pubkey, key.relay_url,
pid, desktop_instance_id (asserted equal to app.config().identifier),
started_at. Final disk assertions verify last_started_at.is_some(),
last_stopped_at.is_none(), last_error.is_none() on the matching record.
Removing the post-spawn record save drops last_started_at and fails the
disk assertions.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Fix all five IMPORTANT defects identified by Thufir's P4 pass-1 review:
1. Concurrency tests: both test_compensate_drain_writer_vs_compensation_deterministic
and test_compensate_drain_concurrent_start_is_blocked now drive the production
compensate_drain adapter (not compensate_drain_for directly). Transition guard
passed by value; store lock and restore owned by the production symbol.
Coordinator/contender ordering via channels/barriers exclusively — no
thread::sleep.
2. Global-restart tests: cross-platform spawn_long_lived_child_for_test (sh loop /
ping) and spawn_noop_child_for_test (sh exit 0 / cmd.exe exit 0) replace the
UNIX-specific sleep 10000 and /usr/bin/true, fixing Windows Rust CI failures.
Full-tail test asserts non-empty personas/teams/global in captured context.
Context-load-failure test uses malformed JSON (not absent file) to exercise the
genuine parse-error path. Record-mesh-change test seeds eligible runtime and
constructs mismatched context.mesh_model_id to trigger in-epoch TOCTOU guard.
3. Active-scope identity import: import_identity routes through the production
with_workspace_transition_preflight (mesh-llm) / direct workspace_transition
(no mesh-llm) when has_active_scope, matching apply_workspace orchestration.
Direction tests rewritten as helper-vs-helper: each side uses a production
helper (with_workspace_transition_preflight or install_client_under_workspace_
transition); oneshot channels establish entry/release ordering with no direct
lock acquisition and no sleep.
4. Team memory boundary: setup_team_import_app_with_scope adds a memory-bearing
team member (member_with_memory); seam test asserts both the captured HTTP
relay URL and the captured owner p-tag from the built engram event after
after_store commits a distinct owner/scope.
5. CI portability: setup_import_app_with_scope and setup_team_import_app_with_scope
use WorkspaceAgentScope::new with writable temp agent base so definitions_dir
has the production <base>/scopes/<scope_id> shape, fixing the Linux /retention
EPERM failure and the poisoned-lock cascade in seam tests.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Resolve clippy::unnecessary-map-or in import_tests.rs from the P3
completion commit. No logic change.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Area 4 — capture scope BEFORE discovery in ensure_relay_mesh_for_record.
Previously capture_active_scope was called after resolve_mesh_bootstrap_target;
a workspace switch between discovery and capture would let the helper install a
client against an endpoint discovered under the old scope while validating
against the new scope. Move capture_active_scope above the discovery call so
the captured scope and discovered endpoint are always from the same workspace.
Area 4 — make with_workspace_transition_preflight production-callable.
Change the transition body parameter from FnOnce() -> Result<T> (sync) to
FnOnce() -> BoxFuture<'static, Result<T>> (async) so production callers that
dispatch spawn_blocking and await the result keep the workspace_transition guard
alive across the full async body. Extract apply_workspace_body as a standalone
async fn; apply_workspace (mesh-llm feature) routes through
with_workspace_transition_preflight so the helper is no longer test-only dead
code. The cfg_attr(not(test), allow(dead_code)) annotation is removed.
Update the two test call sites to pass BoxFuture-returning closures.
Area 3 — add memory-bearing fixture and owner-coordinate assertion.
Add minimal_agent_snapshot_json_with_memory which carries one core memory entry
(MemoryLevel::Core). Rewrite test_agent_switch_between_store_and_profile_finishes_captured_outbound
to use this fixture so submit_memory actually fires in Phase 4. The injected
MemoryPublish adapter now asserts both that relay_url contains the captured
relay URL and that the built engram event's p tag (owner counterpart/coordinate)
equals the captured owner's pubkey hex, not the post-switch owner committed
in after_store. Add extract_p_tag_from_event_json helper.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
mesh_llm.rs was 1016 lines (split-count 1017, gate limit 1000). Extract
MeshReadinessFailure, classify_mesh_readiness_failure,
mesh_readiness_failure_message, and wait_for_mesh_inference into a new
mesh_llm_readiness.rs submodule (140 lines). mesh_llm.rs is now 887 lines
(split-count 888). All other files remain under the 1000-line limit.
Tests that used the readiness items via use super::* add explicit
use super::readiness::* imports since the items moved out of the parent
namespace. No behavior change.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
apply_workspace and import_identity now call run_mesh_transition_preflight
(extracted from fail_if_client_mesh_active path) rather than inlining the
check. ensure_relay_mesh_for_record captures scope before discovery and
routes the install call through install_client_under_workspace_transition,
which validates full scope identity (scope_id, relay, owner, generation)
under the workspace_transition guard before invoking the closure.
with_workspace_transition_preflight gains run_mesh_transition_preflight as
a companion: tests continue to call the callback helper directly; production
callers that need spawn_blocking dispatch use run_mesh_transition_preflight
after acquiring workspace_transition themselves.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
hydrate_keys and persist_agent_keys both call the OS keychain through the
SecretStore global singleton. In headless test environments (CI, locked
keychain) these calls block on the macOS Security daemon IPC
(SecKeychainFindGenericPassword / SecKeychainItemModifyAttributesAndData)
and cause tests to hang indefinitely.
Three tests in the Phase-3 seam suite were hanging:
- test_record_mesh_change_after_preflight_aborts_before_stop
- test_full_tail_stop_spawn_receipt_register_save
- test_relay_mesh_preflight_precedes_stop
All three write agent records with non-empty pubkeys to disk then call
load_managed_agents_at or save_managed_agents_at, triggering keychain IPC.
Fix: gate hydrate_keys and persist_agent_keys with #[cfg(test)] early
returns. Test builds exercise the keychain-generic testable cores
(hydrate_keys_with / persist_agent_keys_with) via mock KeyStore impls
directly; the production wrappers are not exercised in unit tests and
must never be. Also add SecretStore::warm_cache_for_test for use by any
future test that needs to pre-seed the in-process cache without touching
the OS keychain.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Buzz Desktop release v0.5.5
- **Frozen main:** `25a9cf1be6d245fbd7373cb1160dbc790baf5bd5`
- **Reviewed candidate:** `8380c1f8ead8816bcf1f4ea9f66aa08e2441b15a`
- **Previous desktop release:** `desktop-v0.5.4`
- **Proposed immutable tag:** `desktop-v0.5.5`
This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.
The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Adopt spawn_hash → spawn_snapshot rename from main; migrate spawn_config_hash: 0
test fixtures to prospective_spawn_config_snapshot() calls in global_agent_config
tests, epoch tests, and runtime_commands tests.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Buzz Desktop release v0.5.5
- **Frozen main:** `4a2305170eef565bf1836e2859247e67c030f8af`
- **Reviewed candidate:** `2d03d37b05b68186b2caad9da79080032be3ac72`
- **Previous desktop release:** `desktop-v0.5.4`
- **Proposed immutable tag:** `desktop-v0.5.5`
This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.
The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Four test files exceeded the 1000-line gate introduced by Phase 3 seam work.
Split each by extracting the largest block into a sibling file included via
#[path], keeping every file at or below the limit:
- global_agent_config.rs (1987 → 946): inline test block moved to
global_agent_config_tests.rs (637) + global_agent_config_epoch_tests.rs (420)
- mesh_llm_tests.rs (1052 → 776): Area-4 serialization tests extracted to
mesh_llm_transition_tests.rs (284)
- team_snapshot/tests.rs (1087 → 896): Area-3 phase-boundary seam tests
extracted to team_snapshot/seam_tests.rs (200)
- runtime_commands_tests.rs (1056 → 777): concurrency tests extracted to
runtime_commands_concurrency_tests.rs (289)
All nine files are now under 1000 lines. No test logic changed; all 2230
lib tests pass. just desktop-check passes at this commit.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
- items_after_test_module: move SCOPE_GENERATION_TEST_LOCK before mod tests in scope.rs
- await_holding_lock: add #[allow] + SAFETY comments on all tests holding
std::sync::Mutex across .await (single-threaded tokio runtime, no deadlock risk)
- needless_borrow: drop redundant & on &handle calls to async import cores
- too_many_arguments: suppress on spawn_agent_child_at (8-arg function required
by approved Area-5 signature; bounded to two call sites)
- redundant_closure: replace wrapper closures with function-item references
for stop_managed_agent_process and write_agent_runtime_receipt
- unused_imports: remove unused load_managed_agents_at from two test imports
- cloned_ref_to_slice_refs: replace &[x.clone()] with std::slice::from_ref(&x)
in three test save_managed_agents_at calls
- doc_lazy_continuation: add two extra spaces to continuation lines in the
team_snapshot.rs docstring list item 3
- dead_code: suppress on make_captured_scope helper prepared for future tests
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- preserve Databricks catalog 401 responses as authentication failures
and retry discovery exactly once after silently refreshing the rejected
bearer
- preserve runtime OAuth recovery: when discovery has no usable OAuth
credential, `session/new` succeeds with only the trimmed configured
model so the first `session/prompt` can run the existing browser PKCE
flow
- reject a rejected configured `DATABRICKS_TOKEN` with actionable,
non-interactive guidance; static credentials cannot recover through PKCE
- use the configured-model fallback for non-auth discovery failures
without caching failed or fallback catalogs, so later sessions retry
discovery
- keep known Databricks v2 models only for authenticated empty-catalog
responses and mark their provenance
- resolve discovery before MCP spawn or session registration, preventing
failed discovery from leaking resources or consuming session capacity
- permit serialized interactive PKCE only from the explicit saved-agent
model picker; passive draft discovery never opens a browser
## Runtime flow
1. OAuth discovery attempts cached credentials and silent refresh
without opening a browser.
2. If no usable OAuth bearer exists, `session/new` advertises only the
configured model and succeeds.
3. The first `session/prompt` uses `TokenSource::bearer()`, which may
launch browser PKCE.
4. A later session retries discovery and caches only the authenticated
catalog.
## Regression coverage
- rejected-but-locally-fresh OAuth bearer performs one refresh and one
catalog retry
- OAuth mode with no cached token allows `session/new` and returns
exactly the trimmed configured model
- the OAuth fallback is not cached; a later authenticated session
retries discovery and caches the returned catalog
- rejected static tokens still reject `session/new`
- failed discovery does not consume the sole session slot or spawn the
supplied MCP process
- Desktop interactive/passive auth intent, static-token redaction, and
authenticated empty-catalog provenance
## Verification
- `cargo test -p buzz-agent`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib
commands::agent_models`
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
--all-targets -- -D warnings`
- `cargo fmt --all -- --check`
- `git diff --check`
- full pre-push hooks
## Review
Adversarial review found and drove fixes for session/MCP resource
leakage, duplicate concurrent PKCE flows, sensitive error propagation,
incorrect 403 reauthentication, missing discovery-level coverage,
passive browser launch, and the Desktop file-size ratchet. The final
follow-up preserves the existing prompt-time OAuth flow while retaining
static-token rejection and pre-allocation discovery ordering.
---------
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Adds three tests to commands/mesh_llm_tests.rs exercising the lock-serialization
contract between with_workspace_transition_preflight and
install_client_under_workspace_transition:
- test_active_client_stop_then_transition_preflight_succeeds: installs a mock
client, calls production mesh_stop_client (clears the runtime slot), then
calls with_workspace_transition_preflight; asserts fail_if_client_mesh_active
sees an absent runtime and the transition body executes.
- test_transition_held_queued_install_detects_stale_scope: holds workspace_transition
directly, advances the generation counter and commits a distinct scope; spawns
an install task that queues on the lock; after the guard drops, install_client_under_
workspace_transition acquires, detects the stale captured scope (generation +
full identity mismatch), and returns Err without invoking the injected install
closure.
- test_install_held_transition_preflight_observes_client: holds workspace_transition
directly and places a mock client runtime in AppState; spawns a transition task
that queues on the lock; after the guard drops, with_workspace_transition_preflight
acquires, runs fail_if_client_mesh_active, observes the installed client, and
returns Err.
All three tests call the production helpers directly. No port (127.0.0.1:9337) is
touched. Generation-sensitive tests acquire SCOPE_GENERATION_TEST_LOCK to avoid
cross-test interference.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Area 3 — snapshot import phase seams:
- Extract confirm_agent_snapshot_import_core and
confirm_team_snapshot_import_core, generic over tauri::Runtime.
- Add before_store (post-entry-capture, pre-Phase-3a-lock) and after_store
(post-Phase-3a-lock, pre-Phase-3b) hooks; no-ops in production.
- ProfilePublish<'a>/MemoryPublish<'a> borrowed arg structs — no secret-key
cloning across closures.
- Thin Tauri commands call cores with no-op hooks and real relay adapters;
app.state::<AppState>() inside async move blocks avoids non-'static borrows.
- Delete duplicate submit_engram_event from team_snapshot.rs; reuse import.rs
version (pub(crate)) for both agent and team engram boundaries.
- Add retain_team_pending_in_scope(scope, team) sibling in teams.rs; team
snapshot Phase 3 calls it instead of live retain_team_pending to avoid
re-resolving active scope after a possible switch.
- Move egress-guard + avatar tests from import.rs into import_tests.rs
(included via #[path]); update egress inventory and allowlists.
- Add SCOPE_GENERATION_TEST_LOCK in scope.rs for cross-module serialization
of generation-sensitive tests; agent + team seam tests share this lock.
- 4 named seam tests all pass concurrently (verified with cargo test --lib).
Area 4 — workspace transition helpers:
- Extract with_workspace_transition_preflight: acquires workspace_transition,
runs fail_if_client_mesh_active, invokes transition_body under guard.
- Extract install_client_under_workspace_transition: acquires
workspace_transition, validates full captured scope identity
(scope_id, relay, owner_pubkey, generation) under guard, calls install.
- Both helpers live in mesh_llm_scope.rs alongside fail_if_client_mesh_active.
- Area 4 tests (3 named) implemented in mesh_llm_tests.rs (separate commit).
Also: remove unused AppHandle import from nest.rs (zero warnings).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- serialize native `openChannel` tray actions with the camelCase field
names consumed by the TypeScript frontend
- prevent a valid tray channel ID from becoming `/channels/undefined`
- add a Rust serialization contract test covering the complete frontend
payload shape
### Root cause
`TrayAction` renamed the enum variant to `openChannel`, but its struct
fields still serialized as `channel_id` and `community_generation`. The
frontend reads `action.channelId`, so tray navigation called
`goChannel(undefined)`.
### Testing
- manually verified the corrected runtime payload and tray navigation
before removing temporary logging
- `just desktop-ci`
- pre-push hooks (desktop checks/tests, Tauri checks, and Rust tests)
---------
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.5
- **Frozen main:** `383d9e1eafd569b44b9c835200dba69ef7cec9dc`
- **Reviewed candidate:** `ac589061ef1009f55384536e483cfe9b1260697b`
- **Previous desktop release:** `desktop-v0.5.4`
- **Proposed immutable tag:** `desktop-v0.5.5`
This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.
The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary
- adopt the finalized NIP-MP project model so one project can enumerate
and switch between multiple NIP-34 repositories
- add project and repository navigation, activity summaries,
existing-repository attachment, and repository access-channel management
- preserve privacy-safe activation provenance for agent-authored
patches, pull requests, issues, and associated commits
## Test plan
- [x] Run desktop typecheck and unit tests
- [x] Run focused NIP-MP, repository access, and provenance tests
- [x] Run Rust formatting and desktop lint checks
- [x] Run the complete pre-push suite after merging current `main`
- [ ] Manually verify project creation, repository attachment,
switching, and access repair on staging
- [ ] Manually verify public-channel and private-agent origin labels on
newly created Git activity
Related: [#4695](https://github.com/block/buzz/pull/4695)
---------
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
CapturedRestartContext struct prepared fallibly before any stop: loads
personas, teams, and global config from captured definitions_dir with ?,
verifies owner keys against captured_scope.owner_pubkey and derives
owner_hex from keys, resolves effective Mesh model ID for pre-stop
preflight. Failure in any pre-stop step returns Skipped without
stopping the running agent.
restart_under_captured_epoch_for: injected stop_fn/spawn_fn/write_receipt_fn
(all FnMut for multi-relay support); core owns key construction, receipt
construction, runtimes.insert with context.scope.scope_id, captured-dir
saves; in-epoch re-resolve of Mesh model detects non-workspace TOCTOU
edits (Skipped before stop); stop failure classified as Skipped/
stop-failed-before-irreversible, not FailedAfterStop.
spawn_agent_child_at: teams: &[TeamRecord] parameter added; live wrapper
loads live teams; captured epoch passes context.teams loaded fallibly
from definitions_dir; internal live load_teams call removed.
Area-1 completion: adds two missing concurrency tests
test_compensate_drain_writer_vs_compensation_deterministic (channel-
established ordering, BOTH effects on disk) and
test_compensate_drain_concurrent_start_is_blocked (channel/barrier,
contender blocked until transition guard released).
Area-2 tests: all six Thufir-named tests implemented and passing
(context_load_failure, mesh_preflight_failure, workspace_switch,
record_mesh_change, full_tail_stop_spawn_receipt_register_save,
relay_mesh_preflight_precedes_stop).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- add a visible Stop control for interrupting agent speech
- make push-to-talk available by default while preserving manual mute
controls
- refine agent management, muted audio states, drawer layering, and
return navigation
- suppress duplicate notification sounds for Huddle messages
## Why
Huddles could trap users behind long agent speech, hide useful agent
controls, and leave temporary Huddle state visible after the call. The
drawer also regressed when the terminal substrate began painting behind
the rounded app surface.
## Validation
- `just desktop-ci`
- focused Huddle Playwright coverage for the drawer, speech
interruption, agent picker, and leave navigation
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Overview
Both local archive settings — "Archive my agents' observer frames" (kind
24200) and "Archive my agents' turn metrics" (kind 44200) — previously
defaulted to OFF in OSS builds, controlled by build-time env vars. This
had an irreversible cost: observer frames are ephemeral (not stored by
the relay), so any missed events are permanently unrecoverable. This PR
makes both settings default to enabled for all builds and removes the
build-time flag machinery entirely.
## What changed
### Rust
- `observer_archive_default_enabled()` — returns `true` unconditionally;
removed `option_env!("BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT")`
check and `nest_is_dev()` runtime fallback.
- `agent_metric_archive_default_enabled()` — returns `true`
unconditionally; removed
`option_env!("BUZZ_DESKTOP_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT")` check
and its OSS-build test.
- `build.rs` — removed both `rerun-if-env-changed` declarations
(`BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT`,
`BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT`) and the two baked-env
emitting blocks.
### Build / CI
- `Justfile` — removed `desktop-tauri-test-compiled-flags` recipe (the
dual-compile test machinery).
- `.github/workflows/ci.yml` — removed the "Desktop Tauri compiled-flag
verification" CI step.
### TypeScript
- `useObserverArchiveSeed.ts` — removed `observerArchiveDefaultEnabled`
dep from `ObserverArchiveSeedDeps` and the `policyOn` gate in
`reconcileObserverArchive`; the function now unconditionally calls
`mergeSaveSubscriptionKinds`.
- `useAgentMetricArchiveSeed.ts` — removed
`agentMetricArchiveDefaultEnabled` dep from `AgentMetricArchiveSeedDeps`
and the `defaultOn` flag-check path in `maybeSeed`; the
`hasExplicitChoice` guard is preserved as the sole gate against
re-seeding.
- `LocalArchiveSettingsCard.tsx` — removed `policy` prop,
`observerPolicy` state, and `observerArchiveDefaultEnabled` fetch from
`ObserverArchiveSection`; toggle is now always enabled (just `toggling`
disables it); removed the stale "Always on for internal builds" copy
branch; removed the `observerPolicy !== false` guard from
`handleObserverToggle`.
- `tauriArchive.ts` — updated JSDoc on both default-enabled functions to
reflect always-true.
- `e2eBridge.ts` — changed both mock defaults from `?? false` to `??
true` so E2E tests without an explicit mock override exercise the real
default behavior.
### Tests
- `useObserverArchiveSeed.test.mjs` — replaced `policyOn` dep with
direct merge dep; updated `test_oss_policy_off_no_merge` →
`test_reconcile_always_seeds_24200`; all cancellation, identity-switch,
and ordering tests adapted.
- `useAgentMetricArchiveSeed.test.mjs` — removed `defaultOn` dep and
`test_oss_build_does_not_seed`; updated
`test_internal_build_unset_seeds_*` → `test_default_enabled_*`;
`hasExplicitChoice` guard tests unchanged.
## Preservation of explicit opt-outs
Users who have previously toggled the setting off are unaffected:
- `useAgentMetricArchiveSeed` skips seeding when
`hasExplicitChoice(pubkey)` returns true (localStorage-persisted per
identity).
- Observer archive reconciliation now unconditionally calls
`mergeSaveSubscriptionKinds`, but a user who already deleted the
subscription can turn it off via the Settings toggle, which calls
`removeSaveSubscriptionKind` — this is the existing explicit opt-out
path, and the toggle is now always enabled (not locked by a policy
flag).
## Result
- No `BUZZ_BUILD_*_ARCHIVE_DEFAULT` /
`BUZZ_DESKTOP_BUILD_*_ARCHIVE_DEFAULT` references remain outside
CHANGELOG/history.
- Desktop node tests: 4168 pass, 0 fail.
- `just desktop-tauri-check`: clean.
- `just desktop-tauri-test`: all pass.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Move managed_agents_store_lock acquisition, scope-generation validation,
and load_managed_agents_at into the compensate_drain adapter; compensate_drain_for
becomes a lock-free testable core that accepts records: &mut [ManagedAgentRecord]
and start_fn: FnMut(&DrainJournalEntry, &mut [ManagedAgentRecord]).
Store guard is held continuously through validate→load→restore→save so any
store-lock-only writer is serialized on the store lock (not the transition
guard). The transition guard is still received by value from the caller to
ensure it stays alive through the entire compensation.
Test coverage:
- test_compensate_for_restarts_stopped_entries_in_order
- test_compensate_for_reports_partial_restart_failure
- test_compensate_for_start_fn_receives_records_slice
- test_compensate_drain_empty_stopped_returns_none_with_real_app
- test_compensate_drain_stale_scope_skips_all_with_real_app
- test_compensate_drain_attempts_restart_and_reports_degradation_with_real_app
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>