Skip to content

fix(desktop): make remotely hosted agents mentionable from other machines - #4023

Open
TanskiSzymon wants to merge 6 commits into
block:mainfrom
TanskiSzymon:fix/remote-agent-mentions
Open

fix(desktop): make remotely hosted agents mentionable from other machines#4023
TanskiSzymon wants to merge 6 commits into
block:mainfrom
TanskiSzymon:fix/remote-agent-mentions

Conversation

@TanskiSzymon

@TanskiSzymon TanskiSzymon commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Related work

Reported in #3277, #2950, #2987 and #2603. Client-side fixes for the same
symptom are proposed in #2605, #2893, #3292, #3472 and #3676 — this PR differs
in also publishing kind:10100, without which the directory those fixes read
from stays empty (see #3277 for relay-layer confirmation). The Rust half here
is separable and can be rebased on top of whichever client-side fix maintainers
prefer.

Problem

An agent hosted on one machine cannot be @-mentioned from another. This makes the
"run an agent on a always-on box, talk to it from your laptop" setup impossible,
and the failure is silent: the agent shows up in the member list with a presence
dot, but never appears in mention autocomplete and never answers.

Three independent causes, each sufficient on its own.

1. Remote agents were filtered out before the directory rules ran.

useMentions adds relay-directory agents as candidates with isAgent: true, and
the very next filter — isAgentIdentityInManagedList — dropped every agent not in
managedAgentPubkeys, i.e. not running locally. shouldHideAgentFromMentions,
which implements the actual directory rules, could never be reached for a remote
agent. The two are contradictory: one path adds them, the next removes them.

2. Nothing published kind:10100.

list_relay_agents queries kind:10100 and getMentionableAgentPubkeys derives
mentionability from it, but no code path in the desktop or the harness ever wrote
that kind. The directory is empty in every deployment, so the discovery path it
backs is dead code in practice.

3. respond_to: anyone was decided from a source only the agent's host can write.

anyone means "any member of a channel I am in". The check asked the agent's own
directory entry whether it shares a channel with the reader — but only the machine
holding the agent's key can update that entry. It is empty for an agent that has
not republished since starting, and stale whenever someone else changes the agent's
channels. Visibility therefore depended on whether a third party's laptop happened
to refresh in time.

Changes

  • Pass the already-computed mentionableAgentPubkeys into
    isAgentIdentityInManagedList so directory-advertised agents survive the gate.
    The parameter is optional; MembersSidebar, where restricting to locally managed
    agents is intended, is unchanged.
  • Publish the agent's kind:10100 entry from probe_agent_relay_access, which
    already runs per (agent, community) on start and already queries the agent's
    kind:39002 memberships — no extra round trip, and it is signed with the agent's
    own keys as consumers expect.
  • Refresh that entry when the agent joins a channel, and on directory reads
    (throttled per agent), so it does not stay channel-less after creation.
  • Accept the open channel's membership as an equally valid signal for anyone.
    The client already holds it and it is correct the moment the agent joins;
    the directory entry remains as the second source. owner-only and allowlist
    are unaffected — presence in a channel never widens who may instruct an agent.
  • Document the behavior where users choose it, under "Who can send instructions".

kind:10100 carries two contracts

Worth flagging for review. The client reads name, respond_to and channel_ids
from this kind, while the relay's handle_agent_profile requires
channel_add_policy and fails the whole side effect without it:

ERROR Side effect failed: kind:10100 missing channel_add_policy field

So an entry written for the client is rejected by the relay, and vice versa. This
is likely why the directory was never populated. The entry here satisfies both,
publishing owner_only — the relay's own default, so behavior is unchanged. A
cleaner fix is to split the concerns, but that is a protocol change and out of
scope here.

Testing

  • desktop: 3889 unit tests pass, including new cases for the eligibility gate,
    membership-beats-stale-directory, and that presence never overrides owner-only.
  • desktop/src-tauri: agent_events 11/11, covering the entry shape,
    channel_add_policy, snake_case wire format, and channel-id extraction.
  • cargo clippy --all-targets -- -D warnings clean; biome check clean.
  • Verified end to end on a self-hosted relay across two machines: an agent running
    on a Mac mini is mentionable and answers from a second laptop, under both
    anyone and allowlist, while owner-only stays private.

Note on kind:39002

channel_ids are read from the d tag. AGENTS.md says channels use h tags,
which holds for message events but not for membership events — kind:39002 is
addressable and carries the channel in d, the same shape get_channels relies
on. Calling it out because the guidance reads as universal and cost me a debugging
cycle.

`isAgentIdentityInManagedList` dropped every candidate whose `isAgent` flag
was set unless the agent was in `managedAgentPubkeys`, i.e. running locally
on this machine. That gate ran before `shouldHideAgentFromMentions`, so the
finer directory rules it implements (kind:10100 entry, `respond_to`, shared
channels) could never be reached for an agent hosted elsewhere.

The effect was self-contradictory: `useMentions` adds relay-directory agents
as candidates with `isAgent: true`, and the very next filter removed them
again. An agent running on another machine was impossible to @-mention no
matter how it was configured.

Pass the already-computed `mentionableAgentPubkeys` into the gate so agents
the relay advertises as invocable survive it. The parameter is optional, so
`MembersSidebar` — where restricting to locally managed agents is intended —
keeps its current behavior.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
Clients discover invocable agents through `list_relay_agents`, which queries
kind:10100, and `getMentionableAgentPubkeys` derives mentionability from that
entry's `respond_to` and `channel_ids`. Nothing ever wrote the kind, so the
directory was empty in every deployment and an agent hosted on one machine
could not be mentioned from another.

Publish the entry from `probe_agent_relay_access`, which already runs per
(agent, community) on start and already queries the agent's kind:39002
memberships — so the channel list needs no extra round trip, and the event is
signed with the agent's own keys, as consumers expect (the pubkey is taken
from the event author).

kind:10100 carries two contracts at once: the client-facing directory fields
and the relay's `channel_add_policy`, whose side effect fails outright when
the field is absent. The entry therefore includes `owner_only`, matching the
relay's existing default so behavior is unchanged.

Publishing is best-effort — a failure is logged and never blocks startup,
since the agent still works locally regardless.

Also adds `submit_signed_event_with_keys_at`, so the entry goes to the
agent's own relay rather than whichever community happens to be active.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
The entry published at start time is necessarily channel-less: kind:39002
memberships do not exist until the harness first connects, so a freshly
created agent always advertises an empty `channel_ids`.

That is worse than publishing nothing. `relayAgentIsSharedWithUser` requires
a shared channel for `respond_to: anyone`, so a channel-less entry is never
invocable — and `shouldHideAgentFromMentions` reads directory presence without
invocability as an explicit not-invocable signal, hiding an agent that would
otherwise have been offered.

Refresh the entry from `add_channel_members`, once memberships exist. The
refresh is a no-op for pubkeys that are not locally managed agents, since only
this machine holds their keys, and stays best-effort so a failure never breaks
adding a member.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
kind:39002 is addressable: the channel id is the event's `d` tag and the
members are `p` tags — the same shape `get_channels` already relies on. The
directory entry read `h` instead, so `channel_ids` came out empty for every
agent and the entry advertised the agent as reachable in no channel at all.

`h` is how *message* events scope a channel; membership events are a
different kind with a different tag, and the repo guidance about `h` tags
does not extend to them.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
…tory

Only the machine holding an agent's key can sign its kind:10100 entry, so a
membership change made from another device never reaches it — the agent stays
unmentionable in that channel until its next restart. `add_channel_members`
covers the case where the host itself adds the agent; this covers the rest.

Add `refresh_agent_directory_entries`, invoked from the relay-directory query
so entries are republished exactly when a stale one would be visible.
Throttled per agent (60s) because the caller is UI-driven, and best-effort
throughout: a failure just means the directory is read as-is.

Also documents the behavior where users choose it. "Who can send
instructions" now states that Anyone requires a shared channel, that the
agent runs on the machine that started it, and that it answers only while
that machine is awake with the app open — the three things that otherwise
look like the feature is broken.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
…achable

`respond_to: anyone` means "any member of a channel I am in", but the check
asked only the agent's own directory entry whether it shares a channel with
us. That entry can only be written by the machine holding the agent's key, so
it lags behind reality whenever someone else changes the agent's channels, and
is empty for an agent that has not republished since starting. Visibility
therefore depended on whether the agent's host happened to refresh in time —
something the reader can neither observe nor influence, and which showed up as
a newly created agent being unmentionable right after being added to a channel.

Accept the open channel's membership as an equally valid signal. The client
already holds it, and it is correct the moment the agent joins. The directory
entry stays as the second source, still covering shared channels whose
membership is not loaded.

`owner-only` and `allowlist` are unaffected: presence in a channel never
widens who may instruct an agent.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
@TanskiSzymon
TanskiSzymon requested a review from a team as a code owner July 31, 2026 22:31
@lolamathematician

Copy link
Copy Markdown

I can offer windows testing of this fix. See: #3277 (comment)

@wrdodd

wrdodd commented Aug 1, 2026

Copy link
Copy Markdown

Independently traced this on main and can confirm all three causes — in particular #2, which is what makes the gate-only fixes inert.

The only production writer of kind:10100 on main is cmd_set_add_policy (crates/buzz-cli/src/commands/channels.rs:1005), and it publishes {"channel_add_policy": policy} — nothing else. No respond_to, no channel_ids, no name. Everything downstream then degrades to a non-signal:

  • agents_from_events defaults a missing channel_ids to [] (desktop/src-tauri/src/nostr_convert.rs:473)
  • fromRawRelayAgent maps a missing respond_to to null (desktop/src/shared/api/tauri.ts:684)

So relayAgentIsSharedWithUser returns false on both of its branches for every entry the directory can currently contain, getMentionableAgentPubkeys collapses to the managedAgentPubkeys it was seeded from, and a patch that only widens the addCandidate gate to consult mentionableAgentPubkeys admits exactly the set the old gate already admitted.

Searched all of main for 10100 and KIND_AGENT_PROFILE: 13 files, and the only writers are the CLI policy command above and crates/buzz-test-client/tests/e2e_relay.rs. The agent harness crates have no hits at all — nothing publishes an agent's own profile.

For triage, the open queue splits along exactly that line. Most of the PRs address the gate half; I read #4058 closely and it is inert on its own for the reason above, and the same argument applies to any gate-only change. #3708 is deliberately scoped to the publisher half and states the same finding ("nothing has ever written to it... a no-op in practice"). This PR is the only open one I found that closes both halves in a single change.

So the shortest path to a user-visible fix is either this PR, or #3708 plus one gate PR. Either way the publisher half cannot be skipped. Six open issues describe the resulting symptom: #2508, #3125, #3277, #3671, #3809, #3971.

Reported by a user hitting this in a multi-machine setup: agents show in the member list with presence, but never appear in autocomplete from any machine other than the host.

@TanskiSzymon

Copy link
Copy Markdown
Contributor Author

Thanks — Windows testing would be genuinely useful. I only verified this on macOS (Apple Silicon and Intel), across two machines against a self-hosted relay, so a third platform closes a real gap.

Coming back to this with the context I should have gathered before opening: there are already several PRs against the same symptom — #2605, #2893, #3292, #3472, #3676 — plus #2603, #2950, #2987 and #3277 describing it. I didn't check first, which was my mistake.

Having read through them, one difference is worth flagging, and it's the reason this one is larger:

Every other PR fixes only the client side. They correct the gate ordering in useMentions / agentAutocompleteEligibility so a directory agent survives eligibility. None of them touch the Rust side, so nothing publishes kind:10100 — which matches what @surfingdev noted in #3277 ("the client change alone is a no-op without the 10100 publish") and what @rrickels confirmed at the relay layer.

This PR does both halves:

  • publishes the agent's kind:10100 entry from probe_agent_relay_access, reusing the kind:39002 query it already makes;
  • satisfies both contracts that kind currently carries — the client reads respond_to/channel_ids, while the relay's handle_agent_profile requires channel_add_policy and logs Side effect failed without it. That conflict is likely why the directory stayed empty everywhere;
  • treats channel membership as an independent proof of reachability for respond_to: anyone, so eligibility no longer depends on whether the agent's host refreshed its entry in time. That last part also removes the failure mode the client-only PRs still have: an agent whose directory entry is empty or stale is hidden rather than shown, because directory presence without invocability reads as an explicit exclusion.

Verified end to end on a self-hosted relay: an agent running on one machine is mentionable and answers from a second, under anyone and allowlist, while owner-only stays private.

I'm not attached to this being the PR that lands. If maintainers prefer #3676's client-side shape, the Rust half here is separable and I'm happy to rebase it as a follow-up on top of whichever client fix is chosen — the two are complementary, not competing. @Shipitrealgood, happy to coordinate if that's useful.

One thing I'd flag for whoever picks this up: AGENTS.md says channels use h tags. That holds for message events but not for kind:39002, which is addressable and carries the channel in d — the shape get_channels already relies on. I read it as universal and it cost me a debugging cycle.

@wrdodd

wrdodd commented Aug 1, 2026

Copy link
Copy Markdown

Two additions, both verified against main.

The channel_add_policy conflict checks out, and #3708 hit it independently. handle_agent_profile (crates/buzz-relay/src/handlers/side_effects.rs) bails with kind:10100 missing channel_add_policy field when the field is absent, so any publisher omitting it gets its side effect rejected while the event itself is still stored — exactly the "stored but never applied" shape you describe.

#3708 is the other open PR on the publisher half; it is not in the duplicate list above, and it solves this by reading the agent's prior record back rather than sending a fixed default:

pub(super) fn existing_channel_add_policy(events: &[nostr::Event]) -> String

with the stated reason that a fixed value would "rewrite an operator's deliberate owner_only or nobody choice every time the agent starts." Two authors converging on the same constraint from different directions is decent evidence the diagnosis is right, and if you do split the Rust half here, that read-back is worth carrying over.

The AGENTS.md issue is a real doc bug. Verbatim on main:

146: **Channel scoping**: Channels use `h` tags (NIP-29 group tag), not `e` tags.
147: Filters and queries must scope to `h` tags when operating within a channel.

No carve-out for addressable kinds, so reading it as universal is the natural reading. Worth a one-line PR so the next person does not lose the same cycle.

@TanskiSzymon

Copy link
Copy Markdown
Contributor Author

You are right on both counts, and #3708 changes what I think should happen with this PR.

I missed #3708 when I looked for prior work. It solves the publisher half properly, and the read back of the prior channel_add_policy is the part I got wrong. My DEFAULT_CHANNEL_ADD_POLICY = "owner_only" silently rewrites an operator's deliberate nobody or owner_only choice on every agent start. That is a real bug in my code, not a stylistic difference, and it would have shipped quietly because the relay accepts the write and logs nothing. Publishing an offline record on stop is also something I did not handle at all.

So I am not going to argue for my Rust half. #3708 does it better, it came first, and it comes from the person who reported the issue. If maintainers want the publisher, that is the one to take.

What I would like to check before deciding what is left here: does any open PR treat channel membership as an independent proof of reachability for respond_to: anyone?

The reason I ask is that publishing the directory record is necessary but not sufficient. A record published at agent start is always channel less, because kind:39002 memberships do not exist until the harness first connects. That empty record then reads as an explicit not invocable signal in shouldHideAgentFromMentions, so the agent ends up hidden rather than shown, which is worse than having no record at all. I hit this during testing and it looked exactly like the original bug.

My last commit sidesteps it by accepting the open channel's membership as sufficient for anyone, since the client already holds that data and it is correct the moment the agent joins. The directory record stays as a second source. owner-only and allowlist are untouched, with a test pinning that presence in a channel never widens who may instruct an agent.

If that idea is already covered somewhere in the queue, I will close this and there is nothing lost. If it is not, I would rather strip this PR down to just that piece on top of #3708 than keep a competing publisher around.

On the doc bug, I will open a separate one line PR for AGENTS.md. It is uncontested and it does not need to wait for any of this to be resolved.

tlongwell-block pushed a commit that referenced this pull request Aug 1, 2026
The channel scoping note in `AGENTS.md` reads as universal:

> **Channel scoping**: Channels use `h` tags (NIP-29 group tag), not `e`
tags.
> Filters and queries must scope to `h` tags when operating within a
channel.

It holds for events inside a channel, but not for the addressable events
that
describe one. kind:39000, kind:39001 and kind:39002 carry the channel id
in
their `d` tag, which is what `get_channels` already reads.

Taking the existing wording at face value while working on kind:39002
produces
an empty result rather than an error, since those events do carry `h`
tags in
other flows, so the mistake is quiet and costs a debugging cycle. Came
up while
working on #4023.

Four lines, no behaviour change.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
munirad7s added a commit to munirad7s/buzz that referenced this pull request Aug 1, 2026
* fix(desktop): keep thread-open affordance in archived channels (block#4012)

## Problem

Threaded replies "disappeared" from archived Buzz channels: the **"N
replies →"** summary row and the huddle-started **"View thread"** button
vanished, so existing threads were unreachable from the channel
timeline. The thread data was intact — this was a UI gate, not data
loss.

## Root cause

A single `onReply` prop drove two distinct affordances:
- the **compose** affordances (hover "Reply" button, inline reply
target), and
- the **view** affordances ("N replies →" summary row, huddle "View
thread").

`ChannelPane` nulls `onReply` on archived channels to keep them
read-only. That correctly hid composing — but also hid the view
affordances, since they keyed off the same prop.

## Fix

Two independent props, one per concern:

- **`onReply`** drives the compose affordances and is gated on
`archivedAt` — nulled on archived channels, so no new replies can be
started.
- **`onOpenThread`** drives the view affordances and is passed
regardless of archived state, threaded `ChannelPane → MessageTimeline →
TimelineMessageList → MessageRow`.

Opening a thread on an archived channel is read-only: the thread panel's
composer is independently gated via `isComposerDisabled` (includes
`archivedAt !== null`, `ChannelPane.tsx:318`).

### Before
<img width="811" height="794" alt="Screenshot 2026-07-31 at 20 26 00"
src="https://github.com/user-attachments/assets/670d9db4-30da-4c6d-97dc-275b5dbebca8"
/>

### After
<img width="873" height="791" alt="Screenshot 2026-07-31 at 20 28 04"
src="https://github.com/user-attachments/assets/88525231-2539-4eb3-8117-8e58a0cb3855"
/>

## Validation

- `pnpm typecheck` clean
- biome lint clean on touched files
- full `pnpm test` suite green (3885 tests)
- pre-push `branch-skew` / `desktop-check` / `desktop-test` hooks passed

Signed-off-by: Trey Wood <treyw@squareup.com>
Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e <addeb74792f4b7ef0126c3a7ae6d50a5e94fbedf1ad4e2c0620c771232587ec3@buzz.block.builderlab.xyz>

* docs: fix stale kind count, quick-start numbering, and empty Further Reading (block#2613)

## Problem

Three small documentation defects, each verified against the code at
06e3d82:

1. **ARCHITECTURE.md (Event Kinds section)** says `buzz-core` defines
"all 81 kinds". The registry has grown: `ALL_KINDS` in
`crates/buzz-core/src/kind.rs` now has **127** entries (all unique
values). The sentence also says every kind is `pub const KIND_*`, but
registry entries such as `RELAY_ADMIN_ADD_MEMBER` do not use that
prefix.

2. **NOSTR.md Quick Start** numbers its steps 1, 2, 3, 5 — there is no
step 4. PR block#797 (2a03851) collapsed the old steps 1-4 (dropping the
separate "Start infrastructure" step) into 1-3, but the final "Connect
any NIP-29 + NIP-42 client" comment kept its old number 5.

3. **NOSTR.md "Further Reading"** is an empty heading — the section's
only content (a link to `crates/buzz-proxy/README.md`) was removed in PR
block#1321 (14fba21) along with the proxy crate itself, leaving a dangling
header as the last line of the file.

## Fix

1. Reworded the ARCHITECTURE.md sentence to defer to
`crates/buzz-core/src/kind.rs` as the source of truth, with the current
count (127) as an explicit "at the time of writing" snapshot, so the
sentence stays truthful as kinds are added. Also removed the incorrect
`KIND_*`-naming claim.
2. Renumbered the final quick-start step 5 → 4.
3. Populated Further Reading with three durable links: the upstream
nostr-protocol/nips repo, this repo's `docs/nips/` extension documents,
and `ARCHITECTURE.md`.

Docs-only; no code changes, no build impact.

## Verification (each claim ~30 seconds)

- Kind count: `python3 -c "import re;
s=open('crates/buzz-core/src/kind.rs').read(); m=re.search(r'ALL_KINDS:
&\[u32\] = &\[(.*?)\];', s, re.S); print(len([e for e in
m.group(1).split(',') if e.strip()]))"` → 127. All 127 values are
distinct. Non-`KIND_*` entry example: `RELAY_ADMIN_ADD_MEMBER` (kind.rs,
in `ALL_KINDS`).
- Missing step: `grep -n '^# [0-9]' NOSTR.md` on main shows `# 1.`, `#
2.`, `# 3.`, `# 5.` in the Quick Start block; `git show 2a03851 --
NOSTR.md` shows the renumbering that orphaned step 5.
- Empty section: `tail -1 NOSTR.md` on main is `## Further Reading` with
nothing after it; `git log -S'buzz-proxy/README' --oneline -- NOSTR.md`
shows the content removal in 14fba21 (block#1321).

## Links

- `crates/buzz-core/src/kind.rs` — `ALL_KINDS` registry (source of truth
for the count)
- PR block#797 / 2a03851 — introduced the step-numbering gap
- PR block#1321 / 14fba21 — emptied the Further Reading section

Signed-off-by: Sean Gearin <sgearin@gmail.com>
Co-authored-by: Sean Gearin <sgearin@gmail.com>

* docs: note that addressable channel events scope by d, not h (block#4103)

The channel scoping note in `AGENTS.md` reads as universal:

> **Channel scoping**: Channels use `h` tags (NIP-29 group tag), not `e`
tags.
> Filters and queries must scope to `h` tags when operating within a
channel.

It holds for events inside a channel, but not for the addressable events
that
describe one. kind:39000, kind:39001 and kind:39002 carry the channel id
in
their `d` tag, which is what `get_channels` already reads.

Taking the existing wording at face value while working on kind:39002
produces
an empty result rather than an error, since those events do carry `h`
tags in
other flows, so the mistake is quiet and costs a debugging cycle. Came
up while
working on block#4023.

Four lines, no behaviour change.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>

* Nest-MCP war still unbenutzt: Vault + n8n verdrahtet, Approval-Loch gefixt (#3)

---------

Signed-off-by: Trey Wood <treyw@squareup.com>
Signed-off-by: Sean Gearin <sgearin@gmail.com>
Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
Co-authored-by: Trey Wood <edwinjwood3rd@gmail.com>
Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e <addeb74792f4b7ef0126c3a7ae6d50a5e94fbedf1ad4e2c0620c771232587ec3@buzz.block.builderlab.xyz>
Co-authored-by: Sean Gearin <sean@indistinct.ai>
Co-authored-by: Sean Gearin <sgearin@gmail.com>
Co-authored-by: Tansky <108231030+TanskiSzymon@users.noreply.github.com>
munirad7s added a commit to munirad7s/buzz that referenced this pull request Aug 1, 2026
* fix(desktop): keep thread-open affordance in archived channels (block#4012)

## Problem

Threaded replies "disappeared" from archived Buzz channels: the **"N
replies →"** summary row and the huddle-started **"View thread"** button
vanished, so existing threads were unreachable from the channel
timeline. The thread data was intact — this was a UI gate, not data
loss.

## Root cause

A single `onReply` prop drove two distinct affordances:
- the **compose** affordances (hover "Reply" button, inline reply
target), and
- the **view** affordances ("N replies →" summary row, huddle "View
thread").

`ChannelPane` nulls `onReply` on archived channels to keep them
read-only. That correctly hid composing — but also hid the view
affordances, since they keyed off the same prop.

## Fix

Two independent props, one per concern:

- **`onReply`** drives the compose affordances and is gated on
`archivedAt` — nulled on archived channels, so no new replies can be
started.
- **`onOpenThread`** drives the view affordances and is passed
regardless of archived state, threaded `ChannelPane → MessageTimeline →
TimelineMessageList → MessageRow`.

Opening a thread on an archived channel is read-only: the thread panel's
composer is independently gated via `isComposerDisabled` (includes
`archivedAt !== null`, `ChannelPane.tsx:318`).

### Before
<img width="811" height="794" alt="Screenshot 2026-07-31 at 20 26 00"
src="https://github.com/user-attachments/assets/670d9db4-30da-4c6d-97dc-275b5dbebca8"
/>

### After
<img width="873" height="791" alt="Screenshot 2026-07-31 at 20 28 04"
src="https://github.com/user-attachments/assets/88525231-2539-4eb3-8117-8e58a0cb3855"
/>

## Validation

- `pnpm typecheck` clean
- biome lint clean on touched files
- full `pnpm test` suite green (3885 tests)
- pre-push `branch-skew` / `desktop-check` / `desktop-test` hooks passed

Signed-off-by: Trey Wood <treyw@squareup.com>
Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e <addeb74792f4b7ef0126c3a7ae6d50a5e94fbedf1ad4e2c0620c771232587ec3@buzz.block.builderlab.xyz>

* docs: fix stale kind count, quick-start numbering, and empty Further Reading (block#2613)

## Problem

Three small documentation defects, each verified against the code at
06e3d82:

1. **ARCHITECTURE.md (Event Kinds section)** says `buzz-core` defines
"all 81 kinds". The registry has grown: `ALL_KINDS` in
`crates/buzz-core/src/kind.rs` now has **127** entries (all unique
values). The sentence also says every kind is `pub const KIND_*`, but
registry entries such as `RELAY_ADMIN_ADD_MEMBER` do not use that
prefix.

2. **NOSTR.md Quick Start** numbers its steps 1, 2, 3, 5 — there is no
step 4. PR block#797 (2a03851) collapsed the old steps 1-4 (dropping the
separate "Start infrastructure" step) into 1-3, but the final "Connect
any NIP-29 + NIP-42 client" comment kept its old number 5.

3. **NOSTR.md "Further Reading"** is an empty heading — the section's
only content (a link to `crates/buzz-proxy/README.md`) was removed in PR
block#1321 (14fba21) along with the proxy crate itself, leaving a dangling
header as the last line of the file.

## Fix

1. Reworded the ARCHITECTURE.md sentence to defer to
`crates/buzz-core/src/kind.rs` as the source of truth, with the current
count (127) as an explicit "at the time of writing" snapshot, so the
sentence stays truthful as kinds are added. Also removed the incorrect
`KIND_*`-naming claim.
2. Renumbered the final quick-start step 5 → 4.
3. Populated Further Reading with three durable links: the upstream
nostr-protocol/nips repo, this repo's `docs/nips/` extension documents,
and `ARCHITECTURE.md`.

Docs-only; no code changes, no build impact.

## Verification (each claim ~30 seconds)

- Kind count: `python3 -c "import re;
s=open('crates/buzz-core/src/kind.rs').read(); m=re.search(r'ALL_KINDS:
&\[u32\] = &\[(.*?)\];', s, re.S); print(len([e for e in
m.group(1).split(',') if e.strip()]))"` → 127. All 127 values are
distinct. Non-`KIND_*` entry example: `RELAY_ADMIN_ADD_MEMBER` (kind.rs,
in `ALL_KINDS`).
- Missing step: `grep -n '^# [0-9]' NOSTR.md` on main shows `# 1.`, `#
2.`, `# 3.`, `# 5.` in the Quick Start block; `git show 2a03851 --
NOSTR.md` shows the renumbering that orphaned step 5.
- Empty section: `tail -1 NOSTR.md` on main is `## Further Reading` with
nothing after it; `git log -S'buzz-proxy/README' --oneline -- NOSTR.md`
shows the content removal in 14fba21 (block#1321).

## Links

- `crates/buzz-core/src/kind.rs` — `ALL_KINDS` registry (source of truth
for the count)
- PR block#797 / 2a03851 — introduced the step-numbering gap
- PR block#1321 / 14fba21 — emptied the Further Reading section

Signed-off-by: Sean Gearin <sgearin@gmail.com>
Co-authored-by: Sean Gearin <sgearin@gmail.com>

* docs: note that addressable channel events scope by d, not h (block#4103)

The channel scoping note in `AGENTS.md` reads as universal:

> **Channel scoping**: Channels use `h` tags (NIP-29 group tag), not `e`
tags.
> Filters and queries must scope to `h` tags when operating within a
channel.

It holds for events inside a channel, but not for the addressable events
that
describe one. kind:39000, kind:39001 and kind:39002 carry the channel id
in
their `d` tag, which is what `get_channels` already reads.

Taking the existing wording at face value while working on kind:39002
produces
an empty result rather than an error, since those events do carry `h`
tags in
other flows, so the mistake is quiet and costs a debugging cycle. Came
up while
working on block#4023.

Four lines, no behaviour change.

Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>

* Werkzeugbestand steht jeden Morgen im Brief statt nur auf Zuruf (#59)

---------

Signed-off-by: Trey Wood <treyw@squareup.com>
Signed-off-by: Sean Gearin <sgearin@gmail.com>
Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
Co-authored-by: Trey Wood <edwinjwood3rd@gmail.com>
Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e <addeb74792f4b7ef0126c3a7ae6d50a5e94fbedf1ad4e2c0620c771232587ec3@buzz.block.builderlab.xyz>
Co-authored-by: Sean Gearin <sean@indistinct.ai>
Co-authored-by: Sean Gearin <sgearin@gmail.com>
Co-authored-by: Tansky <108231030+TanskiSzymon@users.noreply.github.com>
@Shipitrealgood

Shipitrealgood commented Aug 1, 2026

Copy link
Copy Markdown

@TanskiSzymon "What I would like to check before deciding what is left here: does any open PR treat channel membership as an independent proof of reachability for respond_to: anyone?"

Yes — open PR #3676 treats channel membership as independent reachability for respond_to: anyone.

How it is handled there

#3676 rewrites Desktop shouldHideAgentFromMentions (and drops the managed-list pre-gate on the @ path in useMentions). For external (non-managed) agent-classified candidates the rule is:

  • Not a channel member → hide (add-via-mention left out of scope).
  • Member, then consult the kind:10100 policy map:
    • anyone → offer — no check of directory channelIds / "shared channel" geometry. Membership is the reachability proof.
    • allowlist → offer only if the viewer is on the list (membership does not widen this).
    • owner-only → offer only if the viewer is the NIP-OA-verified owner (membership does not widen this).
    • No directory declaration → offer only to that verified owner (not "show everyone").

Local managed agents stay always offerable (native path), membership not required.

So the race you hit — a start-time record with empty channelIds reading as explicit not-invocable — does not apply under #3676: member + anyone shows, even when the 10100 has no channelIds yet. The field isn't an input on the mention path.

This also satisfies the four-row fixture table in #3277 (mira hidden, quinn hidden, remote anyone member shown, managed shown) — while replacing the one signal in that issue's sketched rules that would still race, since both derive "shared channel" from the directory's channelIds.

Before closing, worth a pass with @4dlt against #3708 on three things here:

  1. The membership-change refresh triggers — the add_channel_members hook and the throttled refresh_agent_directory_entries before directory reads — aren't in fix(desktop): publish kind:10100 agent profile directory records #3708 or anywhere else in the queue. fix(desktop): publish kind:10100 agent profile directory records #3708 publishes on create/start/stop, so a membership change after start isn't reflected until restart, and one made from another device is something the hosting machine can't observe locally at all. Still relevant under any eligibility PR: surfaces with no member list in hand (new-message recipients, the projects agent prompt — and mentions on main today) decide from the record's channel overlap.

  2. If the triggers are kept, two things in this refresh path to look at when rebasing: it rebuilds the record with the fixed owner_only default (the clobber you called out above), and it hardcodes status: "online" with no is-running filter — which would flip fix(desktop): publish kind:10100 agent profile directory records #3708's offline record back on the next directory read. (fix(desktop): publish kind:10100 agent profile directory records #3708's publish_agent_directory takes status as a parameter and does the channel_add_policy read-back.)

  3. The RespondToField copy is orthogonal to the eligibility question — same category as your AGENTS.md one-liner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants