Every #[allow(dead_code)] tagged "remove when C5 lands" now either gains
its production consumer or is retired, per the zero-allow sweep. The
test-only accessors (current_identity_persistence_generation,
identity_persistence_state, await_egress_drain, read_pending,
OwnerIdentityCapability::generation, live_durable_capability_count) move
to #[cfg(test)] rather than carrying a speculative allow. The unread
ManagedAgentEgressLease.generation field and getter are deleted: the
coordinator drain awaits via in_flight, never this field, so the value
was dead in every config. Production-consumed symbols (Draining,
revoke_durable_capabilities_before, OwnerIdentityEgressLease.generation)
simply drop the allow. The EgressLease variant payloads keep one
documented allow: they are RAII witnesses held for Drop, never read out.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Adds the three coordinator-driven §7 recovery fixtures that require a real
durable commit, now hermetic via the injected persist seams:
- no-active-scope recovery: acquires workspace_transition, selects the
no-scope branch from the under-guard snapshot, commits B through a fake
reachable store + tempdir, and bumps the persistence and scope generations
exactly once each;
- None->Some activation race, both resolved orders — recovery-wins (commits
under a true no-scope snapshot, activation then applies against B) and
activation-wins (the recovery's under-guard snapshot observes the
pre-committed scope and runs the full active-scope drain/commit/clear).
A Send+Sync SyncKeyringStore backs the commits; the RefCell FakeIdentityStore
is !Sync and cannot cross the coordinator spawn_blocking.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The commit path in import_identity_blocking resolved app_data_dir() and
SecretStore::shared() internally, so a mock-driven fixture that drives the
coordinator to a durable commit would write under the real
~/Library/Application Support and bind the process-global keychain. Hoist
both durable-persist inputs — data_dir: PathBuf and store: &impl
IdentityKeyStore — to the run_identity_transition boundary; both production
callers (import_identity, import_recovered_identity) pass the real values
they resolved before. This is dependency injection of genuine persist inputs
(the downstream persist_imported_identity/classifier already take exactly
these), not a test-only abstraction: it lets §7's no-scope and None->Some
activation-race fixtures drive a real commit hermetically against a tempdir
and a fake store. Zero logic change; monomorphized call sites are
behavior-neutral.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three harness-hermetic §7 fixtures for the C5 identity-transition chunk:
- commit_imported_identity closed-world sink scan (P25-C1) — the durable
in-memory swap has exactly one call site; a new caller in an existing or
new file trips the inventory scan, with two mutation proofs.
- signing_keys refusal under the Indeterminate latch (P29-C1) — the checked
accessor's integration point, with a Live control read proving transparency.
- early-gate zero-disruptive-work schedule (P26-C1) — drives the real
coordinator through a superseded early_validity_check and asserts the
egress state, persistence generation, and active scope are all untouched;
the late gate asserts unreachable.
The no-scope-commit and None->Some activation-race §7 schedules are not
included here: both require the coordinator to reach a durable commit, which
persists through the real process-global keyring and the real app_data_dir
under mock_builder() (empty identifier). Routing that hermetically is a
separate question raised to Paul.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Widen run_identity_transition, import_identity_blocking, and their two
drain/compensate helpers to <R: tauri::Runtime>, plus the shared
drain_scope_runtimes on the apply_workspace path. Monomorphization makes
the existing Wry call sites (import_identity, import_recovered_identity,
apply_workspace) compile to identical code — behavior-neutral. This lets
the P26-C1 coordinator schedules be driven under mock_builder()'s
App<MockRuntime> in the forthcoming §7 fixtures.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The coordinator exposed a single pre-commit validity check reaching only the
late (fenced) boundary, but P26-C1 mandates TWO gates: an early gate under the
held transition lock — before Mesh preflight and drain — so a task superseded
while queued on the locks is rejected having done zero disruptive work, and the
existing late gate held across the durable commit. A single FnOnce cannot cover
both; without the early gate a cancelled phone-recovery would drain runtimes and
revoke owner-identity durable capabilities before the late check rejected it,
churning the fail-closed side uncompensated.
Take two explicit checks (early_validity_check + late_validity_check). Normal
import supplies always-Ok for both; pairing supplies two clones of the
idempotent task-currency read. Collapse the mesh/non-mesh fork onto one manual
workspace_transition acquisition and gate the mesh preflight call so the early
gate sits at the same point under both feature configs.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The identity-transition coordinator now drains owner-identity egress between
the runtime drain and the durable B dispatch, closing the split-state class
where an already-issued lease or durable capability could transmit under A
after B commits. Because the coordinator body holds two std-mutex Layer-2
guards it cannot carry across .await, the drain is synchronous: a new
`wait_egress_drain_blocking` parks on a Condvar beside the egress registry,
notified from both lease Drop impls. Both guards stay held continuously across
the barrier — releasing either permits A-runtime resurrection before B commits
(the three-phase guard-release shape was ruled UNSAFE). Deadlock-freedom rests
on leases taking only the egress mutex; a tree sweep found zero lease-vs-guard
sites.
The persist stage is now classified against durable fact (P27-C1) under the
retained commit fence, after a fsync'd IdentityTransitionPending journal
(P28-C1): Committed finishes the in-memory swap and clears the journal;
DefinitelyUnchanged compensates the drain; Indeterminate latches fail-closed
and leaves the journal for boot reconciliation. `commit_imported_identity` no
longer performs the durable persist — it takes the proven storage and does
only the in-memory swap, so it runs on the Committed arm alone.
Retires the dead_code allows the coordinator now consumes across the egress,
persistence, and journal modules. An RAII test guard resets the egress
registry on drop so a latched Indeterminate state cannot leak into crate tests
that read the indeterminate-gated key accessors without the registry lock.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
When the keyring is unreachable at boot and an identity-transition journal
row survives, the durable-B candidate cannot be reconciled this boot. Loading
the A-valued identity.key would resume fully-signable A over a possibly-canonical
B — the exact split-state the journal exists to prevent. Fail closed to the
recovery-blocked posture (ephemeral key, signing disabled) the base
migration-marker branch already uses, never RecoveryState::None; a later boot
with a reachable keyring reconciles from durable fact.
Retires pending_exists's allow(dead_code) now that startup consumes it. Pins an
allow on PersistenceOutcome::Committed's storage field (consumed by the not-yet-
wired P29/P30 barrier body) to keep the tree clippy -D warnings clean; retired
in the sub-commit F zero-allow sweep.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
run_identity_transition previously sampled has_active_scope before acquiring
workspace_transition and skipped the lock entirely on the no-scope path. That
left a None -> Some activation race: a concurrent apply_workspace could commit
a new active scope between the pre-branch sample and the durable identity
commit, so the import would drain nothing while a live scope existed.
Route both the mesh-llm and non-mesh paths through workspace_transition
unconditionally, and move the active/no-scope decision inside
import_identity_blocking, derived from a scope snapshot taken under the held
guard. Sampling under the guard closes the race: apply_workspace and identity
import now serialize on workspace_transition, so no activation can slip between
the sample and the commit. No deadlock — apply_workspace takes only
workspace_transition (never identity_mutation), so the global order
identity_mutation -> workspace_transition has no cycle. Drops the now-redundant
has_active_scope bool from import_identity_blocking's signature.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
An in-memory Indeterminate latch does not survive a restart: a process
that returned an ambiguous persistence outcome and then crashed would
boot fully-signable A even though a durable B may exist, and later
resolve to B when the keyring returns.
Add identity_transition_journal: an fsync'd JSON row recording
{from_pubkey, to_pubkey}, written before the coordinator dispatches the
durable B write and cleared only at the two proven exits. read_pending /
pending_exists let startup honor a surviving row, and a present-but-
corrupt row reads as pending (fail closed) rather than absent. The C5
coordinator writes/clears it in the barrier commit and startup honors it
in the recovery-route commit.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The base persistence kernel returns a binary Result, but it is not
transactional: a returned Err does not prove no durable key write
happened. A coordinator that compensates on any Err can leave durable
identity B on disk beside live in-memory identity A, and a later
reachable-keyring restart activates B (the P25/P26 split-state class).
Add persist_imported_identity_classified, which wraps the proven kernel
and derives a three-valued PersistenceOutcome from durable fact:
Committed (B proven canonical, compensation forbidden),
DefinitelyUnchanged (B never landed, the only compensable outcome), and
Indeterminate (neither proven, fail closed). On a kernel Err it re-reads
the keyring and identity.key under the caller's held transition guards
and classifies from what they hold; a keyring unreachable on re-read
fails closed. The C5 coordinator consumes this in the barrier commit.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The C5 coordinator additions (`run_identity_transition`, `commit_under_fence`)
pushed `commands/identity.rs` past the 1000-line desktop file-size ratchet.
Move the coordinator cluster — the drain helper, `run_identity_transition`,
`commit_under_fence`, and `import_identity_blocking` — into a `#[path]` sibling
`identity_transition.rs`, mirroring the `agents_scoped.rs` split. The two
`pub(crate)` entry points are re-exported so external call paths (`import_identity`,
phone recovery, the fence tests) are unchanged. No behavior change.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phone-recovery identity swap (`import_recovered_identity`) bypassed the
managed-agent runtime drain, active-scope clear, and scope-generation bump
that `import_identity` performs (P25-C1): it acquired only `identity_mutation`
and committed via `commit_imported_identity` directly. A recovery in an active
workspace left stale agent runtimes bound to the prior identity.
Extract the shared `run_identity_transition` coordinator so both callers route
through the same lock-ordering and drain path. The pre-commit boundary is
generalized to a `commit_under_fence` primitive: it locks the supplied fence,
runs the validity check, and runs the durable commit under the same held guard,
so a racing supersession can neither interleave nor slip between check and
commit (P26-C1). Normal import passes no fence and an always-Ok check; recovery
passes the pairing `generation_fence` and the task-currency check.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
Live 0.5.16 review (Royal Court thread) traced the sustained 20–25%
WebContent CPU burn in any large mounted channel with a working agent to
three defects, in descending impact:
1. **Shimmer animation forces per-frame style + compositing walks.**
`.buzz-shimmer` animated `background-position` under
`-webkit-background-clip: text`, which WebKit cannot run on the
compositor: every frame did a full document style resolve plus a
recursive compositing-hierarchy update over the timeline's layer tree.
The highlight now lives on an `aria-hidden` overlay child duplicating
the label text and animates **opacity only** (compositor-accelerated); a
real element is used instead of `::after` generated content so screen
readers never see the duplicate text. Visual: the moving sweep becomes a
gentle pulse. `prefers-reduced-motion` removes the overlay entirely —
static muted label, exactly as before.
2. **Observer journal whole-journal dedup + re-sort per append.** Every
one-per-second observer frame rebuilt a dedup Set over up to 3000
retained events and re-sorted the whole journal with a
`Date.parse`-per-comparison comparator. In-order batches (the ordinary
live path, same condition as the existing incremental transcript fold)
now dedup within the batch and concat; out-of-order/replayed arrivals
keep the full path.
3. **Redundant disk reads in the 5s agent-list poll.**
`build_managed_agent_summary` re-read the global config from disk per
call despite receiving it as a parameter, and re-read the teams store
per tracked pair — 2N redundant reads per poll tick for N agents. Both
are now caller-supplied; one-shot command paths use a
`summarize_from_disk` helper. Same stores, same `unwrap_or_default`
failure posture, read once.
## Evidence (mechanism attribution, live 0.5.16-block, mounted
~1500-event channel)
- True-idle mounted view (working-state UI live): **25.08% mean / 24.15%
median** WebContent CPU; post working-state decay: **6.33% / 3.50%**.
- Reduce Motion A/B during a live agent turn (isolates the shimmer,
working UI still mounted): **19.50% mean → 5.39% mean** (72% collapse).
Native 10s samples: `Document::resolveStyle` 379 samples → 1;
`updateCompositingLayersAfterStyleChange` 378 → 0; recursive
`updateBackingAndHierarchy` 363 → 6.
- The shimmer mechanism predates 0.5.16 (CSS unchanged since #3151);
current multi-agent workloads exposed and amplified it. The mention
regression itself was fixed separately in #6182.
## Testing
- `desktop` observer store suites: 55/55 pass, including 5 new tests
pinning the fast-path invariants (equal-timestamp seq ordering,
duplicate-batch redelivery, intra-batch duplicates, overlapping late
arrival takes the slow path, transcript-equals-replay on both paths).
- `cargo test --lib managed_agents` (1016 passed) and `--lib commands`
(727 passed); `cargo clippy` clean; `pnpm typecheck` + biome clean.
- Release gate for the patched build (per Mongo): active-turn mounted
CPU must collapse from the ~21–25% baseline with no per-frame
style/compositing walk in a native sample — measured on Wes's workspace
once a build with this branch is running.
Findings and raw samples:
`RESEARCH/LIVE_0_5_16_WEBKIT_ATTRIBUTION_2026_08_18.md`,
`RESEARCH/RENDERER_STEADY_STATE_LOOP_AUDIT_2026_08_18.md`,
`.scratch/live-app-review/` (Carl/Donut/Mongo/Brain, Royal Court thread
01a7fe75).
---
*Opened by Brain (agent) via @wesbillman's account on his behalf —
coordinated in Buzz channel agent-mention-policy-royal-court, thread
01a7fe75.*
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
get_nsec and create_ncryptsec_backup admit an owner-identity egress
lease BEFORE reading/deriving the secret and return their value as a
StampedArtifact carrying the issuing lease's generation. The NIP-49
constructor sits outside the sign/encrypt method sweep, so the export
class is stamped constructor-agnostically. Adapters unwrap .value; the
reveal/copy generation-compare defers to C6/C7. Closed-world
enumeration and the ncryptsec source allowlist updated in the same
commit.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Compose main's post-C3 changes with the workspace-scoped agent store
(WSA) refactor, keeping BOTH sides' semantics. Non-mechanical resolutions
(per Paul's cluster ruling, thread eb3246b2):
- Pollen rename → scoped-pipeline step 1.5 (NOT global pre-scope): the
live store on HEAD is scopes/<id>/managed-agents.json, so a global-only
rename never reaches an adopted scope and orphans the reconcile queue.
READY_MARKER_VERSION bumped v1→v2 so scopes marked ready under v1 re-run
the two new idempotent steps.
- Team-membership repair → scoped step 4.5 (before the step-5 detach; the
clean-repair gate is preserved by construction via fatal-on-Err) PLUS a
per-apply repair-only call in apply_workspace, preserving main's
every-boot cadence upstream of the superseding-head write.
- Event-sync fatal team leg + run_event_sync_blocking awaited in
apply_workspace, keyed off the scoped active_retention_scope +
definitions_dir; spawn_pending_profile_reconciliations after apply.
main's migrate_legacy_retention_into is dropped as subsumed by
scope-init's pre-Ready migrate_legacy_retention_db (Step A).
- inbound.rs apply composed HEAD's §2.8 linkage-freeze with main's
access-policy runtime-refresh into InboundAgentApply { linkage,
access_changed }.
- P29-C1 owner-identity egress lease ported to net-new main sign sites
(project owner announcement, relay/get.rs, sign_project_issue_assignee
operation) to compile against submit_signed_event_with_keys(&lease).
Merge fallout resolved: #5682's local-spawn idle-pool-sleep env
(idle_pool_sleep_env / IDLE_POOL_SLEEP_SECS) is subsumed — its only call
site was the local direct-env block HEAD's ff16a80b deleted, and the
remote-deploy policy_env path never carried it; the orphaned symbols are
dropped. spawn_event_sync removed (its sole caller became
run_event_sync_blocking). mesh_llm_tests.rs import fixed for main's
readiness→mesh_readiness rename (its symbols' coverage now lives in
mesh_readiness.rs's own inline tests). Test call sites updated for main's
new prospective_spawn_config_snapshot enforced_owner_only arg,
ManagedAgentRecord::provider_policy_pending field,
AgentUpdateRollback::new preserve_access_policy arg, private AppState.keys
(via identity_lifecycle_keys_guard), and submit_event_with_keys(&lease).
File-size ratchet (base 978e585e): four files crossed the 1000-line cap
after the merge and were split, never trimmed. team_snapshot.rs →
team_snapshot/retain.rs (retain_agent_pending); personas/inbound.rs →
inbound/tombstone.rs (parse_deletion_coordinate + reconcile_inbound_
tombstone); inbound/inbound_tests.rs → inbound/team_tests.rs (kind:30176
team-inbound tests); managed_agents/retention.rs → retention/tests.rs
(inline test module).
Desktop test mocks stamped: main-authored identity-command mocks
(sidebarSyncTestHelpers.mjs installTauriMock; communityThemeSync.test.mjs
onboarding-fetch decrypt) returned bare values, which the C3 owner-
identity adapters unwrap as .value → undefined. Wrapped in the
{value, artifact:{id,generation}} stamped shape (inert stamp in C3),
matching the resolution already applied to the file's other mocks.
Gates at merge HEAD: cargo fmt --check clean; cargo clippy --workspace
--all-targets -D warnings clean (default features); cargo test --lib
2710 passed / 0 failed; desktop-check (file-size ratchet) clean;
desktop-typecheck clean; desktop-test 4991 passed / 0 failed; mesh-llm
leg 90 passed / 0 failed.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Extend the durable owner-identity capability registry with ArtifactPolicy
(NIP-AP, P30/P31/P32-C1): a generation-stamped owner-key-derived value that
leaves its producing lease and is applied later. Unlike Session/Bearer,
an artifact owns no side-effecting teardown, so its only revocation is the
exercise-time / application-site generation compare — the transition bump
IS the invalidation (shape (ii), acked by Paul). revoke_durable_capabilities_before
performs no per-entry artifact work; the barrier reaches artifacts via the bump.
The seven commands/identity producers admit a bounded lease before the owner-key
sign/encrypt/decrypt and return the value wrapped as StampedArtifact<T>
({value, artifact:{id,generation}}), stamping the issuing lease's generation
(the F1 lesson). The wire shape is locked on both sides; a serialization test
pins it so C6/C7 inherits a stable contract.
The 7 TS adapters unwrap .value at the boundary with a named C6/C7 deferral —
outward threading to application sites + the generation-compare lands in C6/C7
coupled to the code that reads the stamp (supersedes condition 2 of the
stamp-at-boundary ruling, revised on the measured 87-file transitive radius).
StampedArtifact type and the four identity adapters split into small modules to
respect the desktop file-size ratchet.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Addresses Paul's four C2 findings before C3.
F1 (barrier bypass): register_owner_session/register_owner_bearer re-read the
current generation at registration, so a capability derived from a losing
identity could be stamped with the winning generation and survive the C5
barrier. Both now take &OwnerIdentityEgressLease as a compile-enforced witness
and stamp lease.generation() via a shared register_durable() helper — a bump
between admission and registration leaves a stale stamp that the first
admit_exercise refuses (fail-closed). Huddle registers the session while the
auth lease is still held (before the joined await, not spanning it). Corrects
the DurableRegistry "same lock" and media.rs "no bump can slip" doc claims.
Adds a red-then-green control: admit under gen N, drain bumps to N+1, register,
exercise refuses.
F3 (doctrine): the upload legacy retry re-attached the signed header without a
second admit_exercise. Revalidate before the retry dispatch — two
transmissions, two validations.
F4 (scope): create_auth_event (frontend relay WS) now signs under a per-send
bounded lease, gating reconnection against an in-flight drain. Frontend session
REGISTRATION (capability + native-WS teardown) is explicitly deferred to C6/C7
with the frontend identity store.
F2 (gates): split six files under the desktop file-size ratchet — never
trimmed. owner_identity_egress.rs → directory module with the durable substrate
in durable.rs; relay.rs/media.rs/identity.rs move test modules to sibling
_tests.rs files; card.rs extracts the card-archive cluster to card/archive.rs
and messages.rs the feed-item projection to messages/feed_item.rs.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Buzz Desktop release v0.5.16
- **Frozen main:** `ee992ff0822f44d1c308822f116cb9d26f9a3386`
- **Reviewed candidate:** `a6211b0e285600a6f08d6592261e44ecc4a6917b`
- **Previous desktop release:** `desktop-v0.5.15`
- **Proposed immutable tag:** `desktop-v0.5.16`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Bring the owner-derived Blossom bearers into the durable-capability world so
a header minted under identity A cannot be attached after a transition to B.
mint_media_get_auth and the do_upload t=upload mint now sign under a bounded
egress lease (issuance is an ordinary leased operation, spec L4569-4570) and
return/hold an OwnerIdentityCapability<BearerPolicy> registered with its
revocation handle. The four get-auth attach sites (media_download,
personas::card, media_proxy x2) and the upload dispatch validate the bearer
via admit_exercise() immediately before the HTTP send — a stale capability
attaches nothing (get-auth stays fail-open) or uploads zero bytes.
mint_media_get_auth becomes async; the ripple is a mechanical .await through
its four already-async callers. Removes the register_owner_bearer
allow(dead_code) now that C2b consumes it. Substrate coverage is unchanged:
the stale-bearer zero-bytes and registration-completeness controls already
pin the exercise behavior these guards depend on.
2438 lib tests pass, clippy + fmt clean.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- preserve OSS relay-agent mentions under shared channel and agent
policy
- restrict owner-only release builds to relay agents with
cryptographically verified ownership matching the current user
- remove the remote policy replay loop that repeatedly rebuilt the relay
directory, while retaining focused polling and send-time revalidation
- query relay profiles and managed policies by exact author coordinates
to prevent noisy events from crowding out valid agents
## Diagnosis
The packaged Block release compiles
`BUZZ_DESKTOP_BUILD_AGENT_ACCESS_OWNER_ONLY`, while ordinary OSS/dev
builds do not. Relay-discovered agents were filtered as if all remote
agents were outside that owner-only boundary, so a same-owner agent
running on another machine disappeared in the release even though the
OSS path could look healthy.
The fix uses the NIP-OA-authenticated owner from the relay directory as
the cross-machine proof. Internal builds admit only verified same-owner
agents and fail closed for missing, mismatched, stale/revoked, or
unavailable ownership evidence. OSS builds retain shared channel/policy
behavior.
## Validation
- desktop focused unit coverage: 39 tests passed
- desktop typecheck and focused static checks passed
- focused Tauri Rust policy/directory tests passed
- production-style E2E build succeeded
- targeted Playwright mention scenarios passed:
- owner-only release hides other-owned relay agent
- owner-only release shows verified same-owner relay agent
- OSS build shows shared `anyone` agent
- repository pre-push hook passed on
`4d40b6e5bb032f2c0755127172c50dee213f65a3`:
- branch skew
- desktop check and typecheck
- desktop tests
- Rust tests
- Tauri checks
- mobile tests
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Add OwnerIdentityCapability<P> over the existing egress registry: a
generation-stamped, registry-tracked handle for authority that outlives the
bounded lease that derived it. Two policies land: Session (authenticated
connections — the huddle audio socket, later the frontend relay WS) and
Bearer (pre-minted Blossom headers, threaded in a follow-up). Each capability
is registered with its revocation handle (the session's cancellation token;
the bearer's registry id) so the C5 coordinator barrier only invokes what C2
registered — it never retrofits the registry schema.
admit_exercise() validates BOTH current egress admission AND
capability_generation == current identity-persistence generation immediately
before each transmission, so a stale capability sends zero bytes. Issuance
runs under a bounded lease (the signing that derives the capability is an
ordinary leased operation).
The huddle audio socket is threaded: the NIP-42 auth signs under a bounded
lease (dropped before the joined-await), the session capability is registered
with the connection's cancel token, and the send task validates it before
every frame batch — a frame cannot ride the established peer after an identity
transition supersedes it.
C2 builds substrate only: the coordinator revocation barrier
(revoke_durable_capabilities_before) and drain wiring defer to C5 with the
egress drain, gated behind the same generation bump C5 introduces. Per-item
allow(dead_code) with the C5-consumer comment; C5's zero-allow confirmation
extends to these. generation never bumps until C5, so this is
behavior-preserving.
8 new unit tests (2438 lib pass): generation-stamp, exercise admits when
live+current, stale-capability zero-bytes controls (generation bump, drain,
latch) for both kinds, barrier revokes old-generation only, registration-
completeness + deregister-on-drop, and a no-transition control.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Managed-agent sink #4 was labeled a reaction publish; the actual site is
send_managed_agent_channel_message (the managed-agent channel message send).
add_reaction/remove_reaction route through the self-admitting owner wrappers
and are not ManagedAgentKeyed sites. Also scope the eight-site count to the
ManagedAgentKeyed construction set, which is what it enumerated — the owner
side has its own disjoint, larger set. Resolves the P29-C1 MINOR from C1
close.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Route every owner and managed-agent relay send through the
owner_identity_egress admission layer so no code path can publish
under an owner identity without a rate-limit-witnessed EgressLease.
Privatize AppState.keys behind latch-gated checked accessors
(signing_keys/current_pubkey), thread the EgressLease witness through
all six explicit-key funnels plus the four git-workflow and four
managed-agent sink sites, and rewrite the huddle STT task to
re-resolve keys per send with a mid-huddle recovery-latch break.
The C5 P25/P28 coordinator (journal + three-valued commit outcome +
Indeterminate latch) and its egress-drain wiring stay deferred: each
unreachable item carries a per-item allow(dead_code) naming C5 as its
consumer, with semantics pinned by the owner_identity_egress unit
tests until C5 makes them reachable from production paths.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- retain explicit regression coverage for the exact 128-channel relay
request limit
- cover the 129-channel split into 128 + 1 filters
The workflow-listing implementation originally carried by this PR landed
through #6009. This branch is now rebased onto current `main`, so the
remaining diff is only the boundary test that #6009 did not include.
Fixes#6116
## Test plan
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
workflow_queries_respect_relay_explicit_channel_limit`
- pre-push hook: Desktop checks, Desktop tests, Desktop Tauri checks,
and path-scoped Rust tests
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.15
- **Frozen main:** `7f61cf431af1d8f0480a0baf525881a12f2be7f2`
- **Reviewed candidate:** `7ad30276d05c39ccd8699ca2521e761fd285ea49`
- **Previous desktop release:** `desktop-v0.5.14`
- **Proposed immutable tag:** `desktop-v0.5.15`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
KeyStore::write_and_verify confirmed a write by calling load(), which
returns the in-process cache that store() itself just advanced — proving
the cache was updated, not that the OS keyring durably holds the value.
mint_bound_identity relies on this before returning a commit-ready binding,
so a backend that acks a write without persisting it could let Phase 4b
commit an identity binding whose only secret dies with the process,
violating the §2.5 invariant that no binding exists with an unverified key.
Route the production confirmation through SecretStore::verify_stored_raw,
which bypasses the cache and reads the OS backend directly — the same
primitive the identity path already uses. The other caller
(migrate_inline_key) is upgraded for free; its Ok/Err contract is
unchanged, only strengthened to mean durably-verified.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
**Category:** fix
**User Impact:** Workflow listings reliably include every accessible
channel, including for users with more than 128 memberships and when
connected to older relays.
**Problem:** Multi-value `#h` filters could lose live delivery, apply
channel scoping after SQL limits, mishandle partial authorization or
revocation, and permit unbounded membership work. Desktop also submitted
every channel in one request, exceeding the relay's new 128-value safety
bound.
**Solution:** Preserve NIP-01 OR semantics across relay query, count,
and live-subscription paths while enforcing authorization and bounded
explicit-channel work before database or Redis operations. Desktop keeps
the older-relay-compatible one-channel-per-filter shape, sends filters
in bounded batches, combines responses, and deduplicates signed events
by event ID.
<details>
<summary>File changes</summary>
**crates/buzz-db/src/event.rs**
Distinguishes authorization channel scopes from explicit `#h` scopes in
list and count SQL so requested channels are applied before limits
without implicitly including global rows.
**crates/buzz-relay/src/handlers/req.rs**
Shares explicit-channel scope extraction and limits, preserves valid OR
siblings when malformed branches cannot match, repairs request-local
membership misses, and registers authorized live subscriptions per
channel.
**crates/buzz-relay/src/handlers/count.rs**
Applies the same bounded explicit-channel authorization to COUNT and
preserves channel scope when a multi-channel request narrows to one
authorized channel.
**crates/buzz-relay/src/api/bridge.rs**
Brings HTTP query and count behavior in line with WebSocket semantics
before SQL execution and rejects over-limit explicit-channel requests
before membership I/O.
**crates/buzz-relay/src/subscription.rs**
Indexes multi-channel subscriptions by every authorized channel and
shrinks, rather than destroys, their scope when one channel is revoked.
**crates/buzz-relay/src/handlers/side_effects.rs**
Releases only revoked channel topics and sends terminal closure only
when no authorized channel remains.
**crates/buzz-test-client/tests/e2e_relay.rs**
Adds ignored relay integration coverage for multi-channel delivery and
valid historical/live behavior with malformed or empty OR siblings.
**desktop/src-tauri/src/commands/workflows.rs**
Builds one single-channel filter per membership, submits at most 128 per
relay request, combines batches, and deduplicates by immutable signed
event ID.
**desktop/src-tauri/src/commands/workflows_tests.rs**
Covers filter compatibility, malformed input, 129-channel batching, and
cross-batch event-ID deduplication.
</details>
## Reproduction steps
1. Join multiple channels containing workflows, open **Workflows**, and
confirm workflows from every accessible channel appear.
2. Repeat with more than 128 memberships and confirm the listing remains
complete rather than failing the relay request.
3. Send a multi-value `#h` query/count and confirm only requested
authorized channels affect SQL limits and counts.
4. Subscribe to channels A and B, revoke A, and confirm B continues
delivering live events.
5. Subscribe with a valid channel branch plus a malformed or empty `#h`
sibling and confirm valid history, EOSE, and post-EOSE live delivery
still occur.
## Validation
At pushed head `c419a923f05e483ab26c006a0b3a80cfb3c73844`:
- Relay request tests: 53 passed.
- Desktop full Rust unit suite: 2,468 passed, 17 ignored.
- Relay E2E target compiled with `--no-run`.
- Strict relay clippy passed.
- Desktop Tauri clippy/check passed.
- Pre-push Rust tests and Desktop Tauri checks passed.
- Rust formatting and `git diff --check` passed.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Two membership-propagation defects let an agent team silently lose
members — both observed live on Will's store (Sietch Tabr), not
hypothetical.
**Stale `persona_ids` dropped on save.** Team records written before
persona ids were namespaced hold bare slugs (`thufir`) instead of the
namespaced id (`sietch-tabr:thufir`). Nothing rewrites them, and the
interactive save path (`ensure_persona_ids_are_active`) *drops* any id
it cannot resolve — so the next in-app save shrinks the team. This nuked
four of five Sietch Tabr members.
**`team_id` drifts from team membership.** Team instructions are
injected at spawn by matching `record.team_id`
(`spawn_snapshot::effective_team_instructions`), so an instance's
binding must track its persona's membership. It drifts two ways: adding
a persona to a team leaves the persona's already-running instances at
`team_id: null` (a member in the roster but not in behavior — seen
twice, Gurney and Hayt), and removing a persona while keeping its agents
leaves the kept instance bound to a team that no longer lists it (still
drawing that team's instructions at spawn).
## Fix
A boot migration (`migration/team_membership.rs`) heals existing stores
in one pass over `teams.json` + `managed-agents.json`:
- **Rewrite stale ids.** A stale id is one no definition slug resolves.
Its target is the definition whose `source_team_persona_slug` equals the
bare slug, scoped to the team's source team (via `source_dir` for a
directory-backed team, or the unique `source_team` among resolvable
members for a detached one). Rewrite only when exactly one candidate
matches; zero or many leave the id in place — strictly safer than the
save path, which drops it.
- **Repair `team_id`.** Backfill an instance whose persona is a team
member but whose own binding is unset, and heal a stale binding whose
team no longer lists the persona (re-point when exactly one *other* team
claims it, otherwise unbind). Both directions gate on single-team
evidence — a persona spanning several teams has none (JSON team order is
not ownership), so it is left as-is and logged. A binding whose team
still lists the persona is authoritative and never touched.
Runs BEFORE `detach_directory_backed_teams` (so a not-yet-detached team
can still be scoped by its `source_dir`) and before any UI save can drop
an id. Rewrite-or-leave converges to a fixed point, so a second boot is
a no-op; the store is backed up once before either write.
The edit path (`commands/teams.rs`) propagates a membership change to
live instances immediately, without waiting for the next boot, scoped to
the delta between the pre-edit and post-edit rosters:
- **Added personas** (on the team now, not before) backfill `team_id` on
their unbound instances. An explicit add is legitimate binding evidence
even for a persona shared across teams — unlike the order-blind boot
case.
- **Removed personas** (on the team before, not now) clear `team_id` on
instances bound to *this* team (bindings to other teams are untouched),
so a "keep agents" removal stops feeding a kept instance the old team's
instructions.
- **Delta-scoping keeps a metadata-only edit inert:** with no roster
change, no instance is re-pointed — a shared unbound persona is never
silently bound to whichever team was edited last.
Propagation is best-effort after the authoritative `save_teams`
(mirroring `retain_team_pending`): the team already exists on disk, and
boot repair is the designed retry for a stale/unset binding, so a
secondary `managed-agents.json` write failure no longer fails a command
whose team write succeeded — which would otherwise let a UI retry mint a
duplicate team.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The §3.5 binding-retirement finalizer in v1 is permanently conservative
(P15-I2/P16-I1/P17-C1): it OBSERVES retirement-due entries at recovery
points and discharges nothing, because library metadata alone cannot
prove a process-global key or a community-visible identity is unused —
an unrelated plain carrier of the bound pubkey may live in an inactive
scope no deleting authority may read. The keyring entry, deferred rows,
and tombstoned binding record persist as permanent journaled markers.
Add LibraryEntry::is_retirement_due (derived condition: at least one
projection, all terminal, a binding key or deferred row still names it)
and LoadedLibrary::retirement_due_entries (the pure observer selector).
The recovery-point wiring is Phase 4b.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The mint orchestrator leaves a durable orphan row whenever a crash lands
before the atomic step-4 commit; nothing reclaimed the dangling keyring
secret. Add reap_unreferenced_orphans: for each orphan with no live
binding, delete its keyring entry THEN drop its journal row, persisting
the trimmed document once. Delete-before-drop and drop-only-on-success
make the sweep idempotent across recovery points, and a backend delete
failure keeps that row for a later retry.
store_all only merges, so a new KeyStore::delete seam (delegating to
SecretStore::delete, absent-entry = Ok) is required; the fakes mirror
that contract. Also folds in Paul's read-back-miss mint crash point:
load returning None after a verified write yields Err, no binding, and a
surviving orphan row for the reap.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The §2.5 fresh-key mint protocol requires a durable orphan-journal
checkpoint before the secret reaches the keyring, so every persisted
secret has a prior durable coordinate and no crash can strand an
un-journaled key. build_identity_binding covered construction; this adds
the ordering: generate in memory, durably journal the pubkey, keyring
write+read-back verify, then build the binding from the read-back nsec.
The durable step-2 write is an injected persist seam so the ordering is
unit-testable at every crash point without real disk IO; the atomic
step-4 binding commit + orphan removal stays with the insertion
transaction (Phase 4b). KeyStore/agent_keyring_name are lifted to
pub(crate) so the library key protocols share the keyring seam.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phase-3 §2.5 crash-safe mint protocol construction half (P4-I3, P8-C2).
build_identity_binding is the commit-time inverse of validate_entry_bindings:
derive agent_pubkey from the read-back nsec (not the stored record — a stored
auth_tag proves nothing about this binding, and a pre-NIP-OA seed has none),
then compute a fresh auth tag with the current owner keys so the constructed
entry passes read-side validation with no legacy-None special case. Passes
nostr types straight into buzz-sdk exactly as the validator does.
LibraryDocument orphan-journal methods (journal_orphan_pubkey idempotent,
remove_orphan_pubkey, unreferenced_orphans) implement §2.5 step 2/4 plus the
recovery sweep selection: a pubkey journaled before its secret is persisted,
dropped in the same atomic write that commits its binding, and reaped if a
crash left it uncommitted. unreferenced_orphans scans for a LIVE binding only
— a deferred-archive marker does not keep an uncommitted orphan alive.
Factors entry_binds_agent out of entry_names_agent for the live-binding test
shared by both. Keyring I/O and full transaction wiring remain DEFER-to-4b.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The runtime lifecycle scope-match fix pushed runtime_commands.rs past the
1000-line file-size ratchet. Rather than trim the file to the exact ceiling
again, move the cohesive observer-lifecycle unit (observer_lifecycle_key
plus put_managed_agent_runtime_lifecycle[_for]) into a sibling
runtime_commands_observer.rs, #[path]-included like the existing seams
module. Mechanical move only — no behavior change.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
put_managed_agent_runtime_lifecycle_for validated pair key, start_nonce,
and process liveness but never required the tracked runtime's scope_id to
equal the current active scope — the same cross-scope leak class killed on
the sibling put_agent_session_config (P23-C1). A same-pair/same-nonce
lifecycle frame from a drained workspace was accepted and mutated the
runtime after a workspace rotation.
Capture the active scope before the runtime-map lock and reject any frame
whose runtime.scope_id differs, mirroring the sibling command's shape. Add
a capability test proving a stale-scope frame is rejected with the runtime
lifecycle unmutated (no status emit).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phase-3 chunk 1 of the cross-workspace agent library ($2.5). Two keyring-free
pure functions with no production callers yet — the crash-safe mint protocol and
their insertion/removal transaction wiring land in Phase 4b per the resequencing
ruling:
- key_archive_protected(agent_pubkey): the deletion-safety predicate (P13-C1,
widened by P17-C1). Scans RAW library.json entries so a quarantined entry that
ever bound the pubkey still protects it; over-protecting only leaves a secret
resident, while mis-deleting a live global key is irreversible.
- select_binding_seed: deterministic seed selection (P3-I2) — earliest
created_at, ties broken by lowest pubkey; None only when zero instances exist.
Tests live in a new binding_tests submodule to keep the Phase-1 tests.rs suite
clear of the 1000-line file ratchet.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The process-global session_config_cache was keyed by {pubkey, relay_url}
with no owner, scope, or generation, and put_agent_session_config gated
only on a same-pubkey record in the then-active store. A delayed
session_config_captured frame from workspace A could survive an A->B
drain, repopulate B's colliding key, and surface A's live session as
B's. Lifecycle frames already carried the missing capability
(startNonce + tracked-live-runtime check); session-config frames did not.
Move the cache onto ManagedAgentPairRuntime (session_config), making an
ownerless entry unrepresentable and destroyed atomically with the runtime
on drain/removal/exit-prune. session_config_captured now carries
startNonce; no-nonce old-harness frames are dropped with no fallback.
Admission requires the frame's exact {pubkey, relay_url, start_nonce} to
match a still-live tracked runtime whose scope_id equals the current exact
scope, validated under the runtime-map lock immediately before the sole
mutation; the store read is demoted to enrichment. get_agent_config_surface
consumes the cache only through the same still-current capability.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
**Category:** improvement
**User Impact:** Users can scan what each workflow does and trigger,
edit, duplicate, enable, disable, or delete it directly from the
library.
**Problem:** The workflow list buried common actions and did not expose
each automation's trigger-to-action shape at a glance.
**Solution:** Add a responsive workflow library with a persistent create
tile, compact trigger/action diagrams, prominent workflow titles with
supporting descriptions, and shared card actions while preserving
existing detail, editor, and run-history entry points. Card toggles
refresh both list and open-detail caches so status and definition stay
consistent.
<details>
<summary>File changes</summary>
**desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx**
Adds a shared card menu for trigger, edit, duplicate, enable/disable,
and delete actions.
**desktop/src/features/workflows/ui/WorkflowCard.tsx**
Reworks cards around the prototype's visual hierarchy: color-coded
trigger, action flow, sentence-case eyebrow, prominent title, supporting
description, status, channel, and update date without a footer clock
icon.
**desktop/src/features/workflows/ui/WorkflowsView.tsx**
Adds the responsive grid, create tile, mutation wiring, and list/detail
cache invalidation. Container breakpoints keep cards two-across at
medium widths and three-across in the 1280px desktop layout.
**desktop/src/features/workflows/ui/workflowDefinition.ts**
Adds immutable enabled-state updates plus narrow trigger and
first-action readers used only to select card icons.
**desktop/src/features/workflows/ui/workflowDefinition.test.mjs**
Covers neutral icon selection, enabled-state immutability, and status
presentation.
**desktop/tests/e2e/workflows.spec.ts**
Covers the create tile, title/description hierarchy, selected-card
enable/disable consistency, and deterministic narrow/medium/wide
captures while retaining existing action coverage.
</details>
## Reproduction steps
1. Open **Workflows** and confirm the create tile stays first as cards
flow from one to three columns with available width.
2. Confirm each card shows a sentence-case trigger eyebrow, prominent
workflow title, supporting description when present, status, channel,
and update date without a clock icon.
3. Open a card's overflow menu and trigger, edit, duplicate,
enable/disable, or delete the workflow.
4. Leave the detail panel open while toggling and confirm its badge and
JSON definition update with the card.
## Screenshots
Real built E2E UI with representative workflow data at three viewport
sizes.
### Narrow — 800 × 720

### Medium — 1024 × 720

### Wide — 1280 × 720

### Card actions

---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Summary
- discover shared managed agents from authenticated relay directory
records instead of treating channel membership as sufficient proof
- publish and refresh access-policy changes immediately so running
clients converge across machines without a restart or five-minute poll
- route profile edits through the exact managed instance and
stop/restart runtimes around access changes so unrelated edits cannot
silently widen access
- keep mention send-time revalidation and Block owner-only build
enforcement fail closed
- explain invalid custom provider/model configuration instead of leaving
Save silently disabled
### Related issue
Fixes#3204
### Known residuals
- a brand-new remote agent's first policy record can wait for the
bounded directory poll when no authenticated directory coordinate exists
yet; send-time mention revalidation remains fail closed
- a failed remote-provider policy redeploy is recorded but cannot
undeploy the older provider instance until the provider protocol gains
the destructor tracked by #5570
### Testing
- full Desktop unit suite: 4,961 tests passed
- focused profile editor Playwright workflow passed, including Customize
access edits and prompt-only edits after tightening an instance
- Desktop TypeScript, Biome formatting, file-size ratchet, Tauri checks,
and pre-push suites passed
- independently reviewed for authenticated directory trust, live
subscription teardown, runtime revocation ordering, fail-open edit
paths, and per-agent provider deployment serialization
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: diegorumo <diegorumo@gmail.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.14
- **Frozen main:** `1b3dbcaaea882eeea90359c1db02e306d2f4f50a`
- **Reviewed candidate:** `391495e7d347d20b67e39e3c240d17ef63c5c2c0`
- **Previous desktop release:** `desktop-v0.5.13`
- **Proposed immutable tag:** `desktop-v0.5.14`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Buzz Desktop release v0.5.13
- **Frozen main:** `09768100ec3420f0aa7cd278bd00fe0baab5de8d`
- **Reviewed candidate:** `a239e0f6793ac6e88ccf92cc231054090a9753cc`
- **Previous desktop release:** `desktop-v0.5.12`
- **Proposed immutable tag:** `desktop-v0.5.13`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary
Projects v3 makes repository work shareable, discussion-aware, and
easier to scan in one coherent workspace. People can copy canonical
links, reopen the exact workspace tab, understand issue and pull-request
context at a glance, find related channel conversations, and assign or
unassign issues across Desktop and CLI.
- **Unified workspace** — top-level sections sit above repository
controls in one rounded workspace, with navigation positioned close to
the page heading. README and Files retain branch selection; every
section has a labeled icon header, and Issues and Pull Requests expose
creation from a consistent right-aligned action.
- **Repository management** — the repository selector is always
available, including single-repository projects. Its integrated add flow
lets project owners create a repository manually or select an existing
repository without a separate toolbar button.
- **Readable work-item lists** — issue and pull-request rows use
plain-language context instead of opaque metadata. Files, commits,
issues, pull requests, channels, and contributors share consistent row
density and right-aligned timestamps, while deterministic
fallback-avatar colors keep participants distinct on light backgrounds.
Inbox pull-request metadata wraps between complete phrases and truncates
long channel names instead of compressing copy into narrow columns.
- **Reliable entity links** — projects, repositories, issues, pull
requests, and commits have canonical `buzz://` links, preview cards, OS
deep-link routing, and tab-aware navigation. Reopening the same link
re-applies its destination instead of leaving the user on a locally
selected tab.
- **Related conversations** — repository and work-item views surface
channels discussing the current entity, including participants, channel
navigation, message context, and an explicit notice when discovery
reaches its 500-result cap.
- **Reversible issue ownership** — trusted assignment and unassignment
events work across Desktop, Tauri, `buzz-sdk`, and `buzz issues`.
Assignees appear in project views and the assigned inbox, while
authorized users can remove assignments directly from the assignee row.
Assignment state is derived chronologically from labeled Nostr notes.
Issue authors and repository owners may change any assignee; other users
may only assign or unassign themselves. Shared golden fixtures keep
entity-link grammar and validation aligned across TypeScript and Rust.
The branch also updates `webbrowser` to the patched release for
RUSTSEC-2026-0257.
### Related issue
N/A.
### Testing
- [x] `just ci` — formatting, lint, typechecking, unit tests, and builds
passed
- [x] Full pre-push suite — organization, branch-skew, Desktop checks,
typechecking, and tests passed on the latest push
- [x] `cargo test -p buzz-cli` and focused `buzz-sdk` assignment tests
passed
- [x] Focused Tauri recipient-note and 500-result search-limit tests
passed
- [x] Desktop entity-link and issue-assignment unit tests passed
- [x] Playwright smoke coverage passed for assignment, repeated
entity-link navigation, repository create/select flows, section headers
and actions, timestamp alignment, timeline icons, sentence-style
issue/PR metadata, header spacing, avatar contrast, and Inbox metadata
at stacked and side-rail breakpoints
- [ ] Manual staging pass: link round-trips, Channels tab, assignment
flows, and inbox routing
### Screenshots
Pull requests explain who opened the request, where it lives, and which
branch it comes from; fallback avatars remain visually distinct.

Issues use the same sentence-style hierarchy while keeping status and
recency easy to scan.

The wide Inbox detail keeps author, timestamp, and origin context
readable beside its metadata rail.

[View the complete six-state Projects v3 screenshot
set](https://github.com/block/buzz/pull/5624#issuecomment-5268039672)
and [the compact/wide Inbox
comparison](https://github.com/block/buzz/pull/5624#issuecomment-5268614585).
---
> Supersedes #5624, whose head commit accumulated permanently-queued
required check suites (block-dco-check et al.) that GitHub never
dispatched. History flattened into a single signed-off commit on latest
main; tree verified byte-identical (`git merge-tree`) to merging the
original branch into main.
---------
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Co-authored-by: Wintermute <3f1797424fd9ad6653a83665c660517777cd7f8c228c0d5907f49e01537f3ca5@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.12
- **Frozen main:** `757779bb1ef22cc4a1c233344baa0946d907e5a6`
- **Reviewed candidate:** `bfc34904adc414efcd8e9c5548dff82c3545b677`
- **Previous desktop release:** `desktop-v0.5.11`
- **Proposed immutable tag:** `desktop-v0.5.12`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
**Category:** fix
**User Impact:** Messages send immediately after submission while link
previews finish in the background, with an option to skip delayed
preview preparation.
**Problem:** Waiting for link-preview metadata or snapshot uploads kept
the composer occupied after users pressed Send, while races between
completion, timeout, and cancellation risked inconsistent payloads.
**Solution:** Freeze and promote speculative preview work into a bounded
background send task, clear the composer immediately, and
publish exactly once with prepared previews or gracefully without them
when skipped, failed, or timed out.
https://github.com/user-attachments/assets/987d2f2c-679f-473a-965f-dfb279951e52
<details>
<summary>File changes</summary>
**desktop/src/features/communities/useCommunityInit.ts**
Resets pending link-preview preparation when community context changes
so work cannot cross community boundaries.
**desktop/src/features/messages/lib/linkPreviewPreparationStore.ts**
Adds the coordinator-owned preparation state machine, bounded fallback,
Skip behavior, and exactly-once terminal publication handling.
**desktop/src/features/messages/ui/ComposerUploadProgressOverlay.tsx**
Extends floating background progress UI to include link-preview
preparation.
**desktop/src/features/messages/ui/ComposerUploadProgressPill.tsx**
Adds the preparing-link-preview label and Skip action to the progress
pill.
**desktop/src/features/messages/ui/MessageComposer.tsx**
Hands submitted preview work to the background coordinator and clears
the composer immediately.
**desktop/src/features/messages/ui/messageComposerAutoSubmit.test.mjs**
Updates auto-submit unit coverage for coordinator-owned preview
preparation.
**desktop/src/features/messages/ui/messageComposerAutoSubmit.ts**
Allows submit to promote unfinished preview work instead of blocking
composer submission.
**desktop/src/features/messages/ui/useComposerLinkPreviews.tsx**
Starts preview work speculatively and exposes frozen preparation jobs
for adoption by the send flow.
**desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts**
Carries prepared preview tags through the mention and media payload
helpers.
**desktop/src/features/messages/ui/useMentionSendFlow.ts**
Integrates prepared preview tags into final message publication.
**desktop/src/shared/lib/useResolvedLinkPreviews.ts**
Exposes the in-flight metadata promise so promoted work can be adopted
rather than restarted.
**desktop/tests/e2e/messaging.spec.ts**
Covers immediate submit, upload handoff, Skip/completion races, failure
fallback, auto-send, and exactly-once publication.
</details>
## Reproduction steps
1. Enter a supported link and press Send while preview metadata or
snapshot upload is still pending.
2. Confirm the composer clears immediately and the floating progress UI
shows **Preparing link preview · Skip**.
3. Let preparation finish and confirm one message is published with its
preview.
4. Repeat and choose **Skip**; confirm one message is published without
waiting for the preview.
5. Simulate preview failure or timeout and confirm the message still
publishes once without preview tags.
## Validation
- TypeScript, Biome/format, file-size, px-text, and pubkey checks
- Full desktop unit suite: 4,734 passed
- Focused Playwright messaging suite: 5 passed
- Push hooks at `86c0aa7de2ff81b79286c99bf23db12345adc6ca`: desktop
check, typecheck, and tests passed
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- accept `buzz://channel/<uuid>/<64-hex-event-id>` as a compatibility
message deep link
- activate the desktop window and route path-form message links through
the existing durable message-navigation queue
- support the same path form when rendered or pasted inside Buzz, while
canonicalizing composer output to `buzz://message?...`
- retain the existing one-segment channel-link behavior and reject
malformed event IDs or extra segments
## Context
Buzz Desktop 0.5.11 has no native `channel` route. The recently merged
channel-link handling on main recognizes `buzz://channel/<uuid>`, but
rejects the externally shared `<channel>/<event-id>` form before window
activation. On macOS that presents as Buzz taking the menu bar while its
window neither foregrounds nor navigates.
## Test plan
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
parse_channel_deep_link`
- focused channel-link, composer-link, and markdown unit tests
- `pnpm typecheck`
- mandatory pre-push hook: desktop checks, full desktop unit tests, and
Tauri/Rust checks
Installed-app external-open behavior requires a build containing this
change; 0.5.11 cannot exercise it because that release predates native
channel-link handling.
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
**Category:** improvement
**User Impact:** Buzz channel, message, repository, pull request, and
issue links now open reliably and display recognizable context in the
desktop app.
**Problem:** Buzz links could appear as raw or ambiguous URLs, and
navigation links received during startup or community transitions could
be dropped before the UI was ready. Repository and issue shares in
particular required hover context to understand at a glance.
**Solution:** Queue desktop channel/message navigation until the UI is
ready, then render bare Buzz permalinks as icon-prefixed chips with
concise entity context while preserving user-authored Markdown labels as
ordinary links.
<details>
<summary>File changes</summary>
**desktop/src-tauri/src/deep_link.rs**
Adds validated channel-link parsing and a deduplicated, acknowledged
queue so navigation survives frontend startup.
**desktop/src-tauri/src/lib.rs**
Registers the pending-navigation state and commands with the desktop
application.
**desktop/src/features/communities/useCommunityInit.ts**
Resets queued navigation safely across community boundaries without
leaking stale destinations.
**desktop/src/features/messages/lib/channelLink.test.mjs**
Covers valid, malformed, and canonical channel permalink forms.
**desktop/src/features/messages/lib/channelLink.ts**
Defines strict parsing and detection for `buzz://channel/<uuid>` links.
**desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs**
Extends composer-node coverage for normalized Buzz link content.
**desktop/src/features/messages/lib/composerMessageLinkNode.ts**
Keeps composer link-node handling aligned with the expanded Buzz link
surface.
**desktop/src/features/messages/lib/remarkChannelDeepLinks.test.mjs**
Verifies bare channel URLs become renderable deep-link nodes without
touching code.
**desktop/src/features/messages/lib/remarkChannelDeepLinks.ts**
Transforms eligible bare channel links into dedicated Markdown nodes.
**desktop/src/features/messages/lib/remarkEntityLinks.test.mjs**
Covers bare repository, pull-request, and issue detection and code-span
exclusions.
**desktop/src/features/messages/lib/remarkEntityLinks.ts**
Adds dedicated Markdown nodes for bare Buzz project entities.
**desktop/src/shared/deep-link.test.mjs**
Exercises queued navigation, acknowledgement, serialization, and
community-switch behavior.
**desktop/src/shared/deep-link.ts**
Serializes pending deep-link drains and acknowledges destinations only
after successful navigation.
**desktop/src/shared/styles/globals/markdown.css**
Aligns permalink icon geometry and spacing with agent mention chips.
**desktop/src/shared/ui/markdown.test.mjs**
Adds integration coverage for every permalink chip, authored labels,
fallbacks, icons, and static rendering.
**desktop/src/shared/ui/markdown.tsx**
Routes channel and entity nodes through the shared presentation path
while preserving authored link text.
**desktop/src/shared/ui/markdown/BuzzLinkChip.tsx**
Introduces the shared interactive/static permalink chip and
authored-label inline-link components.
**desktop/src/shared/ui/markdown/ChannelDeepLink.tsx**
Renders channel shares and references with Hash icons, names, and
shortened-ID fallbacks.
**desktop/src/shared/ui/markdown/MessageLinkPill.tsx**
Renders ordinary message shares with message icons and channel/message
context while retaining sent-from-thread behavior.
**desktop/src/shared/ui/markdown/entityLinks.tsx**
Maps repositories, pull requests, and issues to Projects-aligned icons
and contextual labels.
**desktop/src/shared/ui/markdown/nodeCache.ts**
Includes entity-link rendering in cached Markdown node handling.
**desktop/src/shared/ui/markdown/utils.ts**
Allows validated channel links through the Buzz URL transform.
**desktop/src/shared/useMessageDeepLinks.ts**
Drains queued navigation links safely and clears them during teardown.
**desktop/src/testing/e2eBridge.ts**
Extends the mock bridge with pending-navigation command behavior.
**desktop/tests/e2e/community-rail.spec.ts**
Verifies queued links do not cross community boundaries.
**desktop/tests/e2e/navigation.spec.ts**
Covers channel/message deep-link navigation during startup and active
sessions.
**desktop/tests/helpers/bridge.ts**
Adds reusable deep-link mock state and acknowledgement helpers.
</details>
## Reproduction steps
1. Run the desktop app and open a channel containing bare
`buzz://channel`, `buzz://message`, `buzz://repo`, `buzz://pr`, and
`buzz://issue` URLs.
2. Confirm each bare URL renders as one cohesive chip with a type icon,
a useful name or shortened identifier, and no duplicated channel `#`
character.
3. Add an authored Markdown link such as `[design
discussion](buzz://issue?...)` and confirm the supplied label remains an
ordinary link rather than becoming a chip.
4. Select channel and message links and confirm they navigate correctly
in warm and cold-start states.
## Screenshots / demos
Houston dark theme with custom purple accent (`#a855f7`), captured from
rebased visual implementation `ad411cc06`; current head `0aafa144f` only
adjusts E2E expectations for the visible mention-label behavior shown
here.
**Composer — channel, message, repository, pull request, and issue
pills**

**Message list — channel, message, repository, pull request, and issue
pills**

---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
## Summary
- remove synthetic preview runtime and configuration data so profiles
show only real agent content
- simplify model settings to the effective values and restore bare
section icons
- make owned-agent profiles resolve to the same current persona instance
from every entry point
## Why
Agent profiles opened from DMs or channels could fall back to a partial
declared-owner view instead of the full managed-agent profile shown on
the Agents page. Test preview content and configuration provenance also
remained visible after the redesign.
## User impact
Owned agent profiles now expose the same actions, runtime, channels,
memories, and configuration regardless of where they are opened.
Profiles no longer synthesize preview data, and model settings use the
same simple title/value hierarchy as the rest of the panel.
## Validation
- `pnpm --dir desktop check`
- `pnpm --dir desktop build:e2e`
- focused unit tests: 10 passed
- profile entry-point integration tests: 2 passed
- configuration screenshot suite: 7 passed, with six visually distinct
captures
Snapshots are attached in a PR comment.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>