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>
## Summary
- preserve the WebKit-required `Window` receiver when scheduling and
clearing presence subscription retries
- add a receiver-sensitive regression test covering both retry creation
and disposal
## Impact
When subscription opening failed, WebKit rejected the detached timer
call before `retryTimer` could be set. The reconciler's `finally` block
then immediately started another reconciliation because demand was still
unsatisfied and no retry appeared pending. This bypassed the intended
exponential backoff and could repeatedly reopen subscription work during
startup, so the impact was more than console noise.
With the timer receiver fixed, a failed open schedules one bounded retry
at a time (1s exponential backoff, capped at 30s), and disposal cancels
it correctly.
## Validation
- `cd desktop && node --import ./test-loader.mjs
--experimental-strip-types --test
src/features/presence/lib/presenceSubscriptionReconciler.test.mjs` (11
passed)
- `cd desktop && pnpm test` (4,993 passed)
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm check` (passes with existing unrelated warnings)
- pre-push hook on `578b9a0b5c851b21de0b8746238b04dc69032e78` (desktop
check/typecheck/tests, Rust tests, Tauri checks, mobile tests all
passed)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- make the differential file-size ratchet a first-class repository gate
- run the same unfiltered gate from pre-push, `just check`, and CI
- remove hidden file-size coupling from Desktop, Web, and Mobile lint
commands
- isolate ratchet Git subprocesses from hook-exported repository state
## Why
The Desktop ratchet grew to govern `desktop/src-tauri/crates/**`, but
the pre-push `desktop-check` command remained path-filtered to non-Tauri
files. That contract drift allowed a Tauri Rust file-size regression
through local validation.
The ratchet already computes its own merge-base diff, so duplicating
governed paths in Lefthook and CI adds drift risk without meaningful
runtime savings. One root gate owns the policy now.
## Testing
- `just file-size-check`
- oversized untracked probe under `desktop/src-tauri/crates/**` fails
with the 1,000-line ceiling
- file-size core tests with hook-style `GIT_DIR` / `GIT_WORK_TREE`
environment
- `lefthook dump` confirms the unfiltered pre-push command
- mandatory pre-push suite passed on
`3217003db10c84d8a0c5f636ec4c92936777a002`
- `git diff --check`
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
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>
The production adapters unwrap .value from the StampedArtifact wire
shape, but the Playwright bridge mocked sign_event, create_auth_event,
nip44_encrypt_to_self, nip44_decrypt_from_self, and
sign_nostr_identity_binding with bare returns. At runtime the app under
test hit JSON.parse(undefined) at every sign/encrypt site, cascading
into membership-subscribe, read-state, and badge failures across the
smoke suite. Add a local stamped() helper and route every owner-identity
artifact mock (including the C4 nsec/backup cases) through it.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
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>
## Summary
- apply the inactive primary-navigation opacity treatment to every
sidebar destination, including Pulse, Projects, and Workflows
- remove the duplicated Inbox and Agents conditionals so future gated
rows inherit the same hierarchy
- add E2E coverage for all inactive rows and restoration to full opacity
when selected
## Validation
- `pnpm --dir desktop build:e2e`
- `pnpm --dir desktop exec playwright test badge.spec.ts --grep "primary
navigation rows share the same inactive emphasis" --project=smoke`
- pre-push hook: desktop check, typecheck, and 4,984 unit tests
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
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>
Agent profiles resolve through one shared selector (`pickProfileAgent`)
at every entry point — the persona card, the profile panel, and library
grouping. That selector ranked instances only by active/name, with no
archive awareness, so a relay-archived instance early in file order
could hijack the persona card and the profile panel. The persona card
also recorded a durable pubkey target, which could strand the panel on
an archived identity when the click landed during the archive-snapshot
fail-open window. The profile panel's Runtime → Instances roster had the
same blind spot: it rendered every persona instance raw, so archived
instances appeared mixed in with live ones as if active.
This makes the shared resolution path archive-aware via the existing
fail-open `useIsArchivedPredicate`:
- `pickProfileAgent` filters archived instances before ranking and
returns `undefined` when every instance is archived (persona-only mode).
- `buildUnifiedGroups` drops archived agents from the standalone `Custom
agents` and `Unknown agents` buckets; matched persona groups keep their
full list and rely on the selector's persona-only fallback.
- `useCanonicalManagedAgentProfile` resolves through a pure
`resolveCanonicalManagedAgent` helper that applies the target-provenance
rules: a deliberately requested archived pubkey stays exact (so its
archive controller can unarchive it, even when a live sibling exists),
`preserveRequestedInstance` still pins a Runtime → Instances selection,
and non-archived historical navigation keeps its canonicalization.
- The persona card's main click records a persona target that
re-resolves every render, so it self-corrects to a live sibling after
hydration. Deliberate instance navigation and the runtime-error
affordance keep their explicit-pubkey path.
- The Runtime → Instances roster (`ProfileInstancesSection`) buckets
instances off the same predicate via `bucketPersonaInstances`: live rows
render as before, and archived rows move under a labeled `Archived`
subsection. The instance count reflects both buckets, and archived rows
keep their explicit-pubkey click so unarchive stays UI-reachable (the
deliberate-navigation path above).
The predicate is fail-open (treats every identity as live while the
relay archive snapshot loads) and self-exempt, so a cold start never
hides an identity and a user is never folded from their own client.
While the snapshot is loading, every instance renders in the live list —
nothing hidden, nothing labeled.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
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>
## Summary
Centralizes model capability knowledge — thinking mode, supported effort
levels, wire routes, and human-readable labels — into a single manifest,
`scripts/model-capabilities.json`. Rust and TypeScript each get a small
interpreter that reads the same manifest, replacing hand-maintained
tables scattered across both languages that had already drifted apart. A
capability change is now a data edit, not parallel edits to two code
paths. Supersedes the codegen approach explored in #3603.
A cross-language contract keeps the two interpreters honest:
`scripts/normative-corpus.json` is a golden snapshot generated from the
Rust resolver (103 vectors covering all six capability axes) and
replayed natively in TS. CI fails if either language disagrees with the
corpus or the corpus drifts from the resolver. Regenerate with `just
regen-model-corpus`.
## Behavior changes
- **Effort dropdown for `openai-compat` providers** no longer offers
`max`. The request path always clamped `max` to `xhigh` on the wire, so
the UI stops offering a value that was silently rewritten. UI-only,
wire-identical.
- **Databricks v2 routing (wire-visible):** uncurated endpoint names
carrying a bare Claude code-name segment (e.g. `goose-opus-5`) now route
to the MLflow chat wire instead of Anthropic Messages — they lose
Anthropic prompt caching but still succeed on a valid OpenAI-compatible
wire. Curated `databricks-claude-*` records and any name starting with
`claude` are unchanged. A handful of other uncurated/adversarial name
shapes similarly fall back to MLflow chat instead of pattern-matched
routes; every curated model resolves identically to before, all axes.
- **Curated model labels on the real discovery path.** The Databricks
API returns no display name, so discovery emits the raw endpoint id as
the model `name` (`{id, name: id}`) on every path. `ModelEntry.name` is
now curated at all four construction seams in `buzz-agent` — v2
discovery, v1 parse, the auth-empty default catalog, and the
configured-model fallback — via a read-only `databricks_registry_label`
lookup over the manifest's `databricks_v2` exact records; `id` stays the
raw wire/config value. A known id renders its curated label
(`databricks-gpt-5-5` → `GPT-5.5`), an unknown id passes through
unchanged, and the default-catalog row reads `GPT-5.5 (default
catalog)`. As a defense against older `buzz-agent` binaries and any
harness that echoes ids, `resolveModelLabel` treats a discovered name
equal to the trimmed id as absent and falls through to the registry
tier; a genuinely distinct name (including the suffixed default-catalog
label) still wins.
## Cleanup
Deletes the duplicated capability tables and their tests: the
`config.rs` gpt5 matchers, effort tables, and clamp logic; the legacy
segment-based Databricks v2 route classifier in `llm.rs`; and the TS
hand tables plus `effortTable.fixture.json`. All are replaced by
manifest lookups through the shared resolver — no line of capability
data exists in two places.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary
Collapsing the sidebar left a phantom copy of it painted over the
community/relay rail — opaquely on flat themes (vesper et al., which
made the rail look *removed*), and as ghost fragments (muted search-box
fill, truncated channel-name tails) on the Buzz themes whose chrome is
intentionally transparent for the gradient.
**Cause:** #4281 made the app-sidebar layer `overflow-visible` (the
huddle drawer needs to escape it). That removed the ancestor clipping
the offcanvas collapse relied on: the sidebar slides to `left:
-sidebar-width` but kept painting, exactly over the `z-0` rail (`z-10`
sidebar layer).
**Fix:** the offcanvas-collapsed sidebar container is now `invisible` +
`pointer-events-none`, with `visibility` added to the transition list so
the 200 ms slide-out still animates and the flip happens only at the
transition's end. Theme-independent; no per-theme CSS touched; the
huddle drawer's `overflow-visible` is preserved.
## Before / after
Left 420px of the app with the sidebar collapsed. Before = unpatched
`origin/main` @ 69107dc3b; after = this branch. Same seeded state, same
build pipeline (`build:e2e` between checkouts).
| theme | before (ghost sidebar over the rail) | after (rail clean: A /
B / + visible) |
|---|---|---|
| vesper |

|

|
| buzz |

|

|
| buzz-dark |

|

|
Before shots: ghost `⌘K` search chip + blue active-item pill painted
over the rail column; on vesper the opaque panel hides the rail buttons
entirely. After: the rail's community buttons (A, B) and `+` are visible
and clickable in all three themes.
Reported by Thomas P in #buzz-bugs:
buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=9ea401ca1d009f555ca4324e136f8d8d8156db2f8afa3ff89fd038d2c16260f7
cc @klopez4212 — this touches the layout your #4281/#5478 work shaped;
please confirm it doesn't defeat the huddle drawer or glass intentions.
The change deliberately hides only the *offcanvas-collapsed* container,
nothing in the expanded path.
## Test plan
- [x] New Playwright regression spec `sidebar-offcanvas-rail.spec.ts`
(buzz / buzz-dark / vesper): collapsed sidebar must be `visibility:
hidden` + `pointer-events: none`, community rail stays visible and
interactive. **Fails on unpatched build** (verified), passes with the
fix.
- [x] Full desktop unit suite: 4,954 pass / 0 fail
- [x] `pnpm typecheck`, `pnpm check` (biome + file-size ratchet +
px-text + pubkey-truncation) green
- [x] Before/after screenshots above captured via the e2e harness on
both builds
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Co-authored-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>