mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
🤖
## Summary
In Buzz Desktop, you could own an agent running on another device but be
unable to mention it in a channel where it had not yet joined: it was
filtered out before you could invite it. A selected agent could also
disappear from the message's recipients when permissions changed. This
lets you select an eligible agent in the existing **@ menu**, invite it
from the message composer, and send to that agent—or see an error and
keep your draft rather than silently sending without it.
#### Where the experience changes
| Screen / control | Before → after |
| --- | --- |
| A channel's **Message #…** composer, or a message's **Reply in thread
to …** composer | Type `@` (or use the existing @ button), choose your
agent, write the message and press **Send message**. An owned agent not
yet in the channel can now reach the existing **“Mention people outside
this channel?”** dialog when its response settings allow you to address
it. |
| That dialog's **Invite** button | Previously the membership
requirement could block the agent before the invitation. Now Invite
checks permission to add it, adds it as an agent member of the
**channel** (not just the thread), then rechecks membership and response
permission before sending the waiting message. An agent already in the
channel needs no invitation. |
| Existing direct message, or the new-message screen with the **To:**
field | A mention is checked against the conversation the message will
actually enter, including a newly created direct message, rather than
the old or not-yet-created destination. This does not add an Invite
control to direct messages. |
| Editing a message / sending attachments | The selected agent remains
part of the send or edit attempt through attachment upload and the final
permission check. Lost permission produces a visible error instead of
dropping that recipient. |
**Invite is not the only chat choice.** The existing **Do nothing**
button sends the message *without inviting or notifying the nonmembers*;
their names remain references in the text. Where you cannot invite, that
choice is labelled **Send anyway**. To abandon the send instead, dismiss
the dialog with Escape. Invitation actions are disabled while
preparation is pending, preventing duplicate clicks.
**Leaving and returning must not resurrect a cancelled send.** Switching
threads or leaving the composer cancels its pending invitation, even if
you return to the same thread. Cancellation before dispatch sends no
message; an accepted membership change cannot be automatically undone.
An ordinary send without a pending invitation remains bound to its
original destination rather than following you into another
conversation.
**Failed sends must not overwrite your next draft.** If you leave a
thread, return and replace or deliberately clear its draft while an
older send is pending, the older failure cannot restore deleted text,
recipients or files; success cannot erase the newer draft—even if its
text is identical. An untouched draft cleared automatically for sending
remains recoverable on failure. This protection also covers reopening
the composer and starting a newer send.
The channel timeline also keeps its existing **new-messages / Jump to
latest** button available when newer messages are waiting to be
displayed. For example, after sharing a reply to the channel and closing
the thread panel, you can click the catch-up button to reveal buffered
messages. Closing the thread does **not** guarantee the shared row
appears automatically or force you away from reading history.
### Related issue
Built on [#7122](https://github.com/block/buzz/pull/7122), base branch
`split/owned-agent-discovery`, which lets Desktop find and verify owned
agents independently of this device. Current integration head:
`1144465d00273cf74b7c22544ae5a3299bd98560`, built on exact published
root `3a56d17824522580fe04cae463b54f4c7ba66021`. Root #7122 has its own
CI and security gates; this PR must not land ahead of that dependency.
Finding an agent is not channel membership, online status or a promise
of a reply. This PR changes what the existing message controls can do
with those agents; it adds no profile, presence, cloud marker or remote
start/stop UI.
Standalone forum post/reply **Invite / Cancel** is added separately in
[#7125](https://github.com/block/buzz/pull/7125); here those composers
only gain visible authorization errors. Same-name selection/binding
fixes ([#7133](https://github.com/block/buzz/pull/7133)) and mention
spacing ([#7128](https://github.com/block/buzz/pull/7128)) are not
included.
Extracted from [#7114](https://github.com/block/buzz/pull/7114)
(historical source `98fe33ec`). [Behavior and draft-recovery
contract](1144465d00/docs/remote-mention-routing.md)
· [Originating
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848).
### Testing

*Earlier candidate, mock desktop browser: the existing channel dialog
now reachable for an eligible owned agent on another device. The two
buttons have different send outcomes; Do nothing is not Cancel.*
[Success and denial
captures](https://github.com/block/buzz/pull/7124#issuecomment-5482292703)
· [Pending-state
capture](https://github.com/block/buzz/pull/7124#issuecomment-5482604329).
These show the relevant UI, not live agent availability, native
authorization or the later draft-storage/catch-up repairs. No
before-state screenshot is available.
Existing coverage exercises exact recipients, invitation
rejection/cancellation, new direct-message destinations, uploads, edits,
thread re-entry and stored-draft deletion. The timeline regression
checks the shared reply becomes visible using the available catch-up
action.
**Integration validation (2026-09-02):** independently reviewed the
routing delta onto root `3a56d178`: seven original patches unchanged;
two reconciliations retain generic publication-error toasts alongside
authorization errors and retain non-authored editability updates. Added
two production-hook regression tests (normal and queued-media
publication) requiring visible generic error, recovered draft and
released pending state.
- Writer validation: **5,995 Desktop tests**, **42 focused tests**, **22
mock-IPC browser journeys** (18 routing, 2 root provenance, 2
destination binding), and **1 voice-note failure journey** passed; lint,
types, size guards and E2E build also passed.
- The broad suite ran before the final formatting-only test amendment,
not as an exact-final-head rerun. Independent AST comparison confirmed
that amendment is semantics-preserving; **4 fresh assertions at final
`1144465d`** passed. Publication rechecked final-head TypeScript,
amended-test formatting and `git diff --check` successfully. No new full
repository `just ci` run is claimed.
- Browser tests use an isolated E2E build and **mock IPC**, not live
relay/native authorization. Historical screenshots above are explicitly
earlier UI evidence, not exact-head runtime certification. Packaged
Tauri/live-relay behavior was not independently witnessed.
- **Published-head gates:** [current CI
run](https://github.com/block/buzz/actions/runs/33657948560) and
[renewed exact-head formal review
request](https://github.com/block/buzz/pull/7124#issuecomment-5513218340)
must clear before landing. [Earlier CI
run](https://github.com/block/buzz/actions/runs/33438436438) and the two
earlier approvals cover `7ffead0f`, not this new head. Root CI/security
clearance remains separate; the independent scoped integration approval
is not merge authorization.
To try it: in a channel or thread, select an owned nonmember agent,
Send, then Invite or Escape and retry. Deny the add or revoke its
response permission before sending: expect a visible error and
recoverable draft, not a message missing the agent. During a pending
send, return to the source thread, edit or clear the draft, then leave
again: late completion must not overwrite that choice.
**Limits:** permission checks and sending are separate operations;
cancellation cannot retract a dispatched message. Draft protection is
same-window, not new cross-window deletion synchronization. Standalone
forum transport failure can still restore text/media without the exact
selected recipients. Native compatibility is inherited: open-source
builds may still recognize a valid legacy, self-declared agent already
in the channel when verified ownership is absent or rejected; that does
not establish ownership or unlock this owned-nonmember invitation path.
Invalid policy from a verified owner is still rejected. No agent
response is guaranteed.
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
88 lines
5.5 KiB
Markdown
88 lines
5.5 KiB
Markdown
# Remote mention preparation and publication
|
|
|
|
Owned relay agents can be selected and invited without local runtime custody.
|
|
The picker and preparation phase evaluate policy but may admit an owned
|
|
nonmember. Publication separately refreshes authorization for the exact eventual
|
|
destination, including a newly created DM. Ownership is not membership and
|
|
cached picker evidence is never publication authorization.
|
|
|
|
Selections are intent: a vanished/revoked key must fail visibly, retain the draft
|
|
and send nothing, rather than silently dropping a recipient. Captured agent keys
|
|
survive composer clearing, media upload, edits and asynchronous preparation.
|
|
A failed local inventory read cannot veto authenticated relay identities or
|
|
admit stale local runtimes. Locally managed runtime readiness remains a separate
|
|
existing path; remote identities never gain synthetic local management records.
|
|
|
|
Chat offers explicit Invite or reference-only send without inviting. Failed adds,
|
|
revoked policy, failed final authorization and cancellation preserve recoverable
|
|
drafts. Standalone forum sends report authorization failures, but standalone
|
|
forum invitation is a subsequent change reusing this phase contract.
|
|
|
|
Native discovery prerequisite: `docs/owned-agent-discovery.md` (PR6).
|
|
Regression coverage: `agentAutocompleteEligibility.test.mjs`,
|
|
`agentMentionRevalidation.test.mjs`, `useMentionSendFlow.helpers.test.mjs`,
|
|
`submitMessageEdit.test.mjs`, `mentions.spec.ts`, and
|
|
`remote-owned-mentions.spec.ts`. The new remote browser fixtures use a single-word
|
|
name deliberately: mention separator behavior belongs to the independent PR1.
|
|
|
|
NIP-OA establishes ownership, not physical hosting, availability, or lifecycle
|
|
control. Final native queries do not provide an atomic relay transaction with
|
|
message publication. Independent review of both native and publication boundaries
|
|
is required before landing.
|
|
|
|
## Invitation intent and cancellation
|
|
|
|
Each chat Invite owns one synchronous pending latch and abort signal, covering
|
|
preparation, inventory, adds and the eventual publication continuation. Escape,
|
|
navigation/unmount, reference-only selection or a replacement prompt invalidates
|
|
that attempt. Clearing the prompt when promoting it to send is not cancellation;
|
|
the signal remains live through final validation and queued media completion.
|
|
Check cancellation after asynchronous preparation and before subsequent mutations
|
|
(including nested local readiness), and again before publication. A completed add
|
|
cannot be undone, but its late response cannot revive cancelled message intent.
|
|
The pending state includes preparation, disabling both Invite and reference-only
|
|
buttons. Cancellation after optimistic clearing restores the captured draft and
|
|
exact mention refs without overwriting newer edits.
|
|
|
|
`useMentionSendFlow.cancellation.test.mjs` drives the actual hooks with React
|
|
StrictMode and deferred dependencies; `remote-owned-mentions.spec.ts` covers
|
|
visible pending, Escape/retry and route navigation at deferred IPC boundaries.
|
|
|
|
### Source draft ownership
|
|
|
|
Invitation intent belongs to one visit of the effective persistence key and
|
|
channel, not just the mounted composer or destination channel. Same-channel
|
|
thread changes and A → B → A invalidate the original visit. The visible owner gates
|
|
optimistic clear, editor recovery and pending state. Storage recovery instead
|
|
consults shared authored intent for the source draft key, even after exit,
|
|
re-entry or unmount. The draft store retains a revision and authoritative-empty
|
|
marker independently of the stored value. Later same-key authored text/deletion
|
|
or a new send revokes older recovery and sent-draft cleanup; editing B does not
|
|
change A's authority. A visit-specific accessor reads that shared authority while
|
|
preserving the separate visible-owner identity. Invalidation makes recovery durable
|
|
synchronously, before a later visit can load or edit the source draft; late async
|
|
completion cannot revive that recovery or release a newer attempt's latch.
|
|
|
|
The editor's authored revision distinguishes an intentional edit → clear from an
|
|
optimistic empty composer. Programmatic send clear/recovery runs inside the draft
|
|
lifecycle's restoration boundary, so it does not mark the source as authoritatively
|
|
deleted. Pending clipboard identity verification settles before exact selected
|
|
mention refs are captured. The source visit and authored revision are captured
|
|
before that wait: an edit, navigation (including A → B → A), or unmount cancels
|
|
rather than reading the new draft's maps. Subsequent asynchronous preparation
|
|
consumes the captured selections.
|
|
Persona preparation similarly consumes captured selections and returns resolved
|
|
refs, writing them into the editor only while the original visit/revision remains
|
|
current. Normal persona creation/reuse and ordinary destination-bound background
|
|
sends retain their existing behavior; accepted membership is never rolled back.
|
|
|
|
Recovery additionally compares stored content/media/exact refs before replacing
|
|
an existing record. Programmatic persistence does not itself change semantic
|
|
intent. Explicit inbox deletion and replacement do; scope reset invalidates old
|
|
handles. This is same-window authority for live continuations, not cross-window
|
|
synchronization or a versioned storage protocol. Reload destroys continuations;
|
|
authored deletion has already removed the durable value. Final membership/policy reads are not atomic with send,
|
|
and cancellation cannot retract an already dispatched publication. Standalone
|
|
forum transport-failure binding recovery and native compatibility remain separate
|
|
review/follow-up boundaries.
|