Before we start

Here are two things Buzz sent to its server. Which one gets saved?

{
  "kind": 9,
  "pubkey": "a41c…",
  "tags": [["h","engineering"]],
  "content": "RC is ready",
  "sig": "…"
}

Sarah posts a message in a channel.

?

{
  "kind": 9030,
  "pubkey": "a41c…",
  "tags": [["p","b7f2…"]],
  "content": "",
  "sig": "…"
}

Sarah adds someone to the workspace.

?

Same format. Same signature. Same server. One of them is never saved.

Curation Labs · Internal Engineering

One envelope,
eight jobs

How Buzz uses Nostr events — starting from nothing, and ending at the relay we are about to fork.

By the end you will

Hold one model of the whole event system.

By the end you will

Know why two identical-looking events behave differently.

By the end you will

Be able to argue about what we add to our fork.

Model · level 0
The anchor

The entire system is three boxes.

Box one
Someone signs a note

A small piece of JSON, signed with a private key so anyone can tell who wrote it.

Box two
The server decides what it is

It checks the note, works out what kind of thing it is, and chooses whether to accept it.

Box three
Something happens

What happens depends entirely on that decision. This is where the surprises live.

Every slide after this one is a zoom into one of these three boxes. Nothing new gets introduced — the picture just gets more detailed.

Zooming box one
Box one · the note

The note is just signed JSON. There is no more to it than this.

{
  "id":         "3f9a…",
  "pubkey":     "a41c…",
  "created_at": 1789600000,
  "kind":       9,
  "tags":       [["h","engineering"]],
  "content":    "RC is ready",
  "sig":        "8b02…"
}
pubkey
Who wrote it. A public key is an identity — no username, no account row.
content
What they wrote. Plain text here; sometimes JSON, sometimes encrypted.
kind
What sort of thing it is. A number. 9 means "channel message."
tags
Labelled extras. Here: which channel it belongs to.
created_at
When they say they wrote it. Their clock, not ours.
id
A hash of the five fields above. Change any of them and the id changes.
sig
A signature over that id, made with the private key.

The signature proves one thing: this key produced these exact bytes. It says nothing about whether any of it is true.

this is the Nostr event format — every Buzz feature is built on this shape

Zooming box two
Box two · the server

In Nostr, that server is called a relay. Ours does far more than relay.

What "relay" normally means

A dumb pipe with a hard drive.

Accept signed notes. Check the signature. Store them. Hand them to anyone who asks. That is the entire job of an ordinary Nostr relay, and it is why the word "relay" was chosen.

What the Buzz relay is

An application server wearing a relay's clothes.

It also enforces membership, runs moderation, executes commands, schedules automation, computes answers on the fly, and signs notes of its own.

Postgres holds the history. Redis pushes live updates. Neither is optional.

Almost everything that confuses people about Buzz comes from expecting the left-hand box and getting the right-hand one.

Model · level 1
Level 1 · who is in box one

Three kinds of author. The third one surprises people.

Box one
Someone signs a note
a personSarah, from her laptop or phone
an agentAn AI agent. It has its own keypair — it is a full participant, not a bot account owned by a human.
the relay itselfThe server signs notes as itself: system messages, membership announcements, automation output.
Box two
The server decides what it is
Box three
Something happens

Remember the third one. When a workflow posts a message on your behalf, it is the relay's signature on it, not yours.

The example we will follow

One message. We will follow it the whole way.

Sarah · #engineering

"RC is ready — @Triage can you check the changelog?"

It is a note

Box one

Sarah's client builds the JSON, fills in the channel and the mention, and signs it with her key.

We just learned this shape.
It must get in

Box two

The relay decides whether to accept it — and that decision has more steps than you would guess.

Act 1 opens this box.
Then things happen

Box three

It gets stored. People see it. An AI agent wakes up. Automation may fire. Four different things.

Act 2 opens this box.

keep this message in mind — every abstract idea in this deck arrives because this message needs it

Zooming box two
Act one · the two fields that decide everything

Before the server can decide, it reads two fields.

Term · defined once

kind

A number that says what sort of note this is. It is not a category or a folder — it is closer to a function name. It tells the server which piece of code should handle this.

9 = channel message · 7 = emoji reaction · 0 = profile
9030 = add someone to the workspace · 30620 = an automation definition

Term · defined once

tags

A list of labelled extras. Each tag is an array whose first item is the label. Three you will see all deck:

h — which channel this belongs to
p — a person or agent this concerns
e — another note this refers to

Sarah's message

kind 9  ·  ["h","engineering"]  ·  ["p","<Triage's key>"]

A message in a channel, mentioning an agent. Three fields, and the whole rest of the talk follows from them.

Model · level 2
Act one · how the decision works

Box two is a list. If your kind is not on it, you are refused.

Box one
Someone signs a note
Box two
The server decides what it is
reads the kind9
looks it upIs 9 on the list of kinds we accept?
finds the permission it needs"you must be allowed to write messages"
if the kind is not on the listRefused. No exceptions, no default behaviour.
Box three
Something happens

This is one function in one file, and it is the front door to the entire product. Everything we ever add has to be added to it.

the list crates/buzz-relay/src/handlers/ingest.rs — required_scope_for_kind()

Zooming box two
Act one · Sarah's message goes in

What the relay actually checks, in plain language.

Is this place open?
check 1Is this workspace still accepting writes at all? (It might be mid-deletion.)
Is this even allowed here?
check 2Some kinds are refused by name, and some are only allowed over one connection type.
Is the note genuine?
check 3Does the signature check out against the id?
check 4Is the claimed time within 15 minutes of ours?
check 5Is the content under 256 KB?
Is it really Sarah?
check 6The key that signed the note must be the key that logged in. You cannot post as someone else.
Is she allowed?
check 7Look kind 9 up on the list → it needs "write messages"
check 8Does Sarah's session actually hold that permission?
Is she banned?
check 9Banned or timed out? Refused here, even if everything above passed.
Then and only then
check 10Hand it to whichever piece of code handles this kind. Nothing has executed until this point.

evidence crates/buzz-relay/src/handlers/ingest.rs — ingest_event_inner()

Zooming box two
Act one · how big is the list?

Buzz has names for 134 kinds. You can write 84 of them.

84
On the list

A client can send these, if it has the right permission.

31
Relay writes these

The server signs them itself. You are refused if you try.

6
Readable only

The app displays them. Nothing can create them.

5
Never stored

Typing, presence — delivered and forgotten.

4
Logging in

Used to prove who you are, not to say anything.

4
Dead

Named, never wired to anything.

gold · what this slide introduces teal · durable, survives a restart coral · the gap or the trap violet · written by the relay itself periwinkle · never stored dashed · named but not built

A list of kind numbers is a dictionary, not a menu. Two-fifths of the words in it are not yours to say.

counted from crates/buzz-core/src/kind.rs × ingest.rs — re-checked by npm run verify

Model · level 3
Act two · opening the last box

"Something happens" is really four questions.

Box one
Someone signs a note
Box two
The server decides what it is
Box three
Something happens
what does it change?Which part of the application reacts
where does it land?A table, a different table, or nowhere
who can read it?Everyone, some people, or only the author
what does it run?Nothing, automation, or an AI agent

These four are independent. Getting a yes from one tells you nothing about the others — which is exactly why the system surprises people.

Zooming box three
Act two · question two

Now we can answer the question from the first slide.

Sarah's message · kind 9

Saved, like you expected.

It goes into the events table as a row. It gets delivered to everyone subscribed to #engineering. It can be searched, replied to, reacted to, and fetched again next week.

Adding a member · kind 9030

Never saved as a message at all.

It is treated as an instruction. The relay changes the membership table, then writes a completely different, new note announcing the result — signed by itself.

The original instruction is not in the events table. Replaying every event would not rebuild your workspace roster.

Some notes are records of something that happened. Some notes are instructions to make something happen. They look identical.

evidence handlers/relay_admin.rs — 9030–9033 mutate membership directly

Zooming box three
Act two · question two, in full

There are seven places an accepted note can end up.

1 · the ordinary one

the events table

Messages, reactions, profiles, files. This is what most people picture when they think "Nostr."

2 · instructions

the command handler

Open a DM, save an automation, trigger one, approve one. The note is kept as a receipt; the real effect is a database change.

3 · roster changes

the membership tables

Add, remove, change role. The instruction is not stored; the resulting state is announced separately.

4 · private

the moderation queue

Reporting someone goes into a table only moderators can read. It never appears in any channel.

5 · private

the feedback table

Product feedback goes somewhere only our operators can see.

6 · enforcement

bans and timeouts

Written as durable enforcement records. Deliberately handled before the ban check, so a timed-out admin can still lift a timeout.

7 · nowhere

delivered and forgotten

Typing indicators, presence. Checked, sent to whoever is listening, never written down.

and outside all seven

files, git, audio

Notes only describe these. The actual bytes live in object storage, git storage and the live audio transport.

"Everything is an event" is a slogan, not a description. A dump of every event is not a backup of Buzz.

evidence handlers/{ingest,command_executor,relay_admin,report,product_feedback,moderation_commands}.rs

Zooming box three
Act two · question two, one more wrinkle

Sarah changes her profile picture twice. How many notes exist?

The intuition

Two. It is an append-only log.

Both versions sit in the table, newest wins on read, and the old one is history you could go back to.

What actually happens

One. The second replaced the first.

Profiles are a "keep only the latest" kind. The relay keeps one note per author per kind, and the superseded one is usually removed from view entirely.

keep every one

Messages

Keyed by
the note's own id
Examples
messages, reactions, forum posts
keep the latest

Profiles, lists

Keyed by
author + kind
Examples
your profile, your mute list
keep the latest, per name

Documents

Keyed by
author + kind + a name you choose
Examples
an AI persona, an automation definition
keep nothing

Signals

Keyed by
Examples
typing, presence

Latest wins by the author's own claimed timestamp — which is why that 15-minute clock check exists.

evidence crates/buzz-db/src/store/replaceable.rs

Zooming box three
Act two · question three

One note. Five people ask for it. Five different answers.

the note

An AI agent records how many tokens a task cost — encrypted to its owner.

the owner

Yes

Because
their key is named in the note's p tag
a teammate

No

Because
their key is not named — the relay refuses the request outright
someone who knows the id

Still no

Because
asking for it by id is separately blocked — knowing it exists is not enough
someone counting

No

Because
"how many are there" leaks too — counts go through the same gate
the search index

Never indexed

Because
these notes are stored with their search text deliberately left empty

Encryption hides the contents. These gates hide the fact that the note exists at all.

evidence crates/buzz-core/src/kind.rs — P_GATED_KINDS, RESULT_GATED_KINDS

Zooming box three
Act two · question three, in full

Four rules about who may read, each checked in five places.

The rule
asking for history
live updates
counting
asking by id
search
Only the author
reminders · push leases · private agent state
author only
author only
author only
author only
withheld
Only people named in it
private DMs · agent metrics · agent activity
must be you
must be you
refused
see next row
not indexed
…even if you know the id
agent metrics · hidden-DM state
checked per note
still refused
Private until the author shares it
AI personas · team catalogs
needs a "shared" tag
same
same
same
same

Sharing a persona is a tag, not a content change — so turning it on does not alter the note's contents, and device sync does not see a phantom edit.

evidence crates/buzz-core/src/kind.rs — AUTHOR_ONLY · P_GATED · RESULT_GATED · SHARED_GATED_KINDS

Zooming box three
Act two · question four

Three different things are watching Sarah's message.

The relay itself
built-in reactionsCertain kinds make the relay update its own state — a new channel creates channel records, a profile updates the user row.
and it may write new notesSystem messages, announcements, updated summaries.
The automation engine
does any workflow want this?Someone may have set up "when a message contains P1, do something."
if so, a run is createdA real database row with a status. Act 5.
AI agents
am I mentioned?Each agent is a separate program, logged in with its own key, watching its channels.
if so, it wakes up and worksAnd then signs its own reply. Act 4.
The agent is not inside the relay. It is a separate process that happens to be logged in.

Sarah pressed enter once. Three independent systems decided, separately, whether to care.

Model · level 3 complete
Act two · the model, at full resolution

You already know the whole model. Here it is with names.

Box one
Someone signs a note
Box two
The server decides
Box three
Something happens
01
Creation

who may sign itbuzz-sdk · desktop

02
Admission

what lets it iningest.rs

03
Transition

what it changeshandlers/*

04
Persistence

where it landsbuzz-db

05
Visibility

who may read itreq.rs · filter.rs

06
Consequence

what it causesevent.rs · workflow · acp

Six independent contracts. Passing one tells you nothing about the other five. From here on, this strip is the map.

the rest of the deck is this strip, lit differently

Model · level 3
Act two · the payoff

One envelope. Eight completely different jobs.

kind 9 · a message

A record

Signed by
a person
Lands in
the events table
Causes
delivery, automation, an agent waking up
kind 0 · a profile

A current value

Signed by
its owner
Lands in
the same slot as last time
Causes
the old one to disappear
kind 41010 · open a DM

An instruction with a receipt

Signed by
a member
Lands in
a table and kept as a receipt
Causes
a structured reply back to the sender
kind 9030 · add a member

A bare instruction

Signed by
an admin
Lands in
the roster only
Causes
a separate announcement, written by the relay
kind 1984 · a report

A private submission

Signed by
any member
Lands in
the moderation queue
Causes
nothing automatic — it is a signal, not a command
kind 20002 · typing

A signal

Signed by
any member
Lands in
nowhere
Causes
a dot to appear, then vanish
kind 22242 · logging in

A proof of identity

Signed by
whoever is connecting
Lands in
the connection
Causes
sending it as a message is refused by name
kind 39006 · a page marker

An answer, not a thing

Signed by
the relay
Lands in
often nowhere — made up on the spot
Causes
it describes state; it is not state

The number tells you which job. The code behind it does the job. That is the first of our three takeaways.

evidence crates/buzz-core/src/kind.rs · handlers/side_effects.rs

Model · level 4 · gates 1, 3, 4, 5
Act three · Agent identity
01
Creation
02
Admission
03
Transition
04
Persistence
05
Visibility
06
Consequence
Wrong model "The agent is the thing I created in Desktop."

"The agent exists" means seven different things.

01 · key
A keypair was minted.
the only durable identity
02 · local
Desktop holds a managed-agent record.
one machine
03 · definition
A persona is published at kind 30175.
author-only unless shared
04 · instance
A managed-agent state event exists at kind 30177.
relay-retained
05 · community
A profile exists in this community.
kind 0 / 10100
06 · roster
The key is a member of channels.
membership tables
07 · runtime
An ACP harness process is actually alive.
presence, 180 s expiry

Delete removes some of these. That is why a deleted agent still shows up in autocomplete.

evidence .ai/analyses/002 · crates/buzz-core/src/kind.rs · docs/remote-agents.md

Act three · The stack

The keypair is the identity. Everything else is a representation.

Definition · 30175
Persona

System prompt, model, provider, avatar, name pool. Owner-signed, addressable at (owner, 30175, slug). Exists before any agent does.

Identity
The keypair

The public key is the agent. Managed agents additionally carry an owner attestation. The body running it is replaceable; the key is not.

Instance · 30177
Managed agent state

Name, linked definition, respond-to policy, parallelism. Siblings: 30176 team · 30178 shareable catalog · 30179 private aggregate (author-only).

Community · 10100 · roster
Local presence

Profile, membership, DMs, jobs. The identity is portable across communities; this layer is not — a protected avatar URL cannot cross the media authorization boundary.

does not propagate
Editing the persona does not mutate a running agent. A live instance consumed its definition when it started.
Runtime
The ACP harness

A process — laptop, pod, provider. Announces liveness with an ephemeral kind 20001. Holds every conversation in memory.

Deployment should be reconciliation toward one live instance per key — not "create another process."

evidence docs/nips/NIP-AP.md · docs/remote-agents.md · crates/buzz-core/src/kind.rs

Act three · Gate 05, for agents

Agent state is the most gated data in the system.

30175

Persona

Gate
shared-tag
Why
system prompts and allowlists must not leak as a side-effect of device sync
30179

Private aggregate

Gate
author-only
Why
existence, count and search matches all withheld from everyone but the author
44200

Turn metrics

Gate
p-gated and result-gated
Why
agent-authored, encrypted to the owner; knowing the id is not enough
24200

Observer frame

Gate
p-gated, ephemeral
Why
live tool/model activity, encrypted to the owner, never stored
30174

Engram

Gate
owner-encrypted coordinate
Why
the agent's memory across sessions; its own ownership protocol
30177 is a sanitized projection — it must never carry the agent's secret key, credentials, or private runtime configuration

Encryption protects the content. The gates protect the fact that the event exists.

evidence docs/nips/NIP-AP.md · NIP-AM · NIP-AE · crates/buzz-core/src/kind.rs

Act three · A design lesson

Absence and failure are not the same state.

Session birth
harnessnew ACP session opens
one queryfetch the agent's kind 30174 core head
decryptvalidate and decrypt with the conversation key
Found
inject<core-memory>…</core-memory> — the agent knows who it is
Confirmed absent
injectonboarding nudge — "no core memory found, ask your user about yourself"
Error
inject nothinga relay outage must not read as "no memory" — an agent told it has no past will cheerfully overwrite a real one

Session creation is never blocked by this. But a failed read is never allowed to look like an empty one.

evidence crates/buzz-acp/src/engram_fetch.rs

Model · level 4 · gates 5 and 6
Act four · Agent-run triggering
01
Creation
02
Admission
03
Transition
04
Persistence
05
Visibility
06
Consequence
Wrong model "If the message posted, the agent got it."

Two gates decide whether an agent wakes. Neither is the relay's.

Author
clientsigns kind 9 with a p tag resolved from autocomplete
submitHTTP POST /events under a NIP-98 proof, or WS EVENT
Relay
ingestten gates, then persist
dispatchRedis publish + local fan-out to matching subscriptions
knowsa key is authenticated and subscribed. Nothing about turns.
Harness · buzz-acp
gate 1 · mentionevent_mentions_agent — is my pubkey in a p tag?
gate 2 · respond_toowner-only (default) · allowlist · anyone · nobody
queuededup, then claim a pool worker for this scope
ACP
sessionreuse this scope's sessionId, or session/new
promptsession/prompt → streamed session/update → stopReason
replyagent signs its own kind 9 and publishes

A mention from a non-owner is dropped by the harness, silently — and the relay's records show a perfectly successful delivery.

evidence crates/buzz-acp/src/{relay,filter,queue,pool,acp}.rs · config.rs — RespondTo::OwnerOnly is the default

Act four · Concurrency

One identity. N workers. The relay cannot tell them apart — by design.

What the relay sees

One pubkey.

Authenticated. Subscribed. Publishing. Every worker in the pool presents as the same Nostr identity, so runtime topology stays private behind the key.

The cost: cross-channel ordering is not guaranteed once more than one worker is enabled.

What the harness runs

A pool, claimed per scope.

The unit of concurrency is the conversation scope — a channel by default, a thread under the thread session policy. At most one prompt is in flight per scope; later traffic queues, merges, or steers.

Three messages in one channel do not fan out to three workers. Three channels do.

01 The binary allows 1–32 subprocesses and defaults to 1.

02 Desktop-managed agents default to parallelism 10.

03 "10" is not ten model conversations — adapters differ.

evidence crates/buzz-acp/README.md · crates/buzz-acp/src/{pool,queue}.rs · desktop managed-agent defaults

Act four · The important one
Wrong model "The relay knows what the agents are doing."

Four things we call "running." One survives a restart.

volatiledurable
Agent is alive

signed kind 20001 presence → Redis · 180 s expiry

ephemeral
ACP session exists

buzz-acp scope maps — sessions, turn_counts, owners

process memory
A turn is in flight

queue in-flight set + ACP client state

process memory
A workflow run

Postgres · RunStatus · semaphore-bounded executor

durable
Ask the relay

"Which workflows are running?"

Answerable. There is a real run model: pending, running, waiting_approval, completed, failed, cancelled.

Ask the relay

"Which agent turns are running?"

Not answerable today. That knowledge lives in whichever harness owns the runtime, with no TTL and no global registry. Restart it and the channel→session mapping is gone.

evidence ARCHITECTURE.md · crates/buzz-db/src/store/workflow.rs · crates/buzz-acp/src/pool.rs

Act four · Arrival during work

A mention arrives mid-turn. Four policies, and three of them cancel.

queue

Wait

Does
hold the new events; deliver after the current turn completes
Cost
latency
steer · default

Weave it in

Does
cancel, then re-prompt with the new message framed as arrived while you were working
Cost
the cancelled work
interrupt

Replace

Does
cancel and re-dispatch, framed as a supersede, for any admitted author
Cost
partial work discarded
owner-interrupt

Replace, but only for the owner

Does
everyone else queues normally
Cost
owner resolution on the hot path
None of these inject into a live turn. They cancel it and re-dispatch a merged prompt.

"Running" disagrees with itself here too: the queue says in-flight, the ACP client says prompt active, the adapter may or may not expose a live run to steer. That disagreement has an error name.

evidence crates/buzz-acp/src/config.rs — MultipleEventHandling · DedupMode::Queue required

Model · level 4 · the whole strip
Act five · Workflows
01
Creation
02
Admission
03
Transition
04
Persistence
05
Visibility
06
Consequence

A workflow exists twice, and the two copies can drift.

Desired state

The signed definition

Kind 30620, addressable, authored as YAML and stored as canonical JSON. This is what Desktop and the CLI read. It is portable, signed, and it is the user-facing truth.

Actual state

The operational row

The relay materializes the definition into a workflows record that the scheduler and executor use. This is what actually runs.

The failure
Ghost workflows

Delete the operational row and leave the signed definition: execution thinks it is gone, the management UI still lists it, and editing the ghost can recreate it.

The shape of the fix
Controller thinking

Signed desired state reconciles to materialized operational state, as one lifecycle operation. The same pattern as agent deployment — converge, don't create.

evidence crates/buzz-workflow/src/lib.rs · ARCHITECTURE.md

Act five · What actually fires
Wrong model "message_posted means any message."

The trigger matcher is narrower than the word suggests.

message_posted

kind 9 only

Does not match
40002 rich message · 40003 edit · 40008 diff
reaction_added

kind 7

Optional
emoji filter, evalexpr filter
diff_posted

kind 40008

For
code-oriented automation
schedule

cron or interval

Path
scheduler scan, not event ingest
webhook

POST /hooks/{id}

Path
HTTP, not event ingest
kind 46020

manual trigger

Path
a signed command through the command executor
Trigger → run
matchkind, then evalexpr filter
re-authorizethe workflow owner's current rights — a removed owner cannot keep automating
create runPostgres row, status pending
semaphoretake a permit or fail fast with CapacityExceeded
runningstep 0, step 1, …

Workflow execution kinds, command kinds, gift wraps and relay-signed workflow output are all excluded from triggering. That is the loop guard.

evidence crates/buzz-workflow/src/lib.rs — trigger_matches_kind() · schema.rs — TriggerDef

Act five · Declared vs. implemented

Seven actions in the schema. Four of them work.

send_message
Posts to the channel, optionally threaded onto the trigger.
implemented
add_reaction
Reacts to the triggering message over HTTP.
feature-gated
call_webhook
POSTs outward — and therefore demands owner/admin authority to save or run.
elevated
delay
In-process pause, capped at 270 s — deliberately under the 300 s step timeout.
not a timer
send_dm
Returns NotImplemented.
declared only
set_channel_topic
Returns NotImplemented.
declared only
request_approval
Produces a suspension and a token — then finalization marks the run failed.
approval_not_supported

Schema, storage, API, CLI, UI and tokens all exist for approvals. The executor cannot yet persist and resume a suspended run.

evidence crates/buzz-workflow/src/executor.rs · lib.rs — RunStatus::Failed, code "approval_not_supported"

Act five · Authority
Wrong model "The workflow posted as me."

The relay signs workflow output as itself.

{
  "kind": 9,
  "pubkey": "<relay key>",
  "tags": [
    ["h", "eng-channel"],
    ["buzz:workflow", "true"],
    ["buzz:workflow-owner", "<owner>"],
    ["buzz:workflow-mention", "<agent>"],
    ["p", "<agent>"]
  ],
  "content": "P1 detected — @Ops please triage"
}

The relay never holds the owner's private key. It signs a new event and attributes the owner in metadata.

buzz:workflow
Loop guard

Excluded from ordinary workflow triggering.

workflow-owner
Policy anchor

Lets a harness apply the owner's policy to a relay-signed message.

workflow-mention
Anti-laundering

Emitted only when that target was authored into the template — never when it arrived through substitution.

Mention resolution is deliberately paranoid: members only, exact display name, greedy-longest and non-overlapping — and if two members share the matched name, no one is woken.

Untrusted trigger text can produce a p tag. It cannot produce a buzz:workflow-mention. Authority must not be launderable.

evidence crates/buzz-relay/src/workflow_sink.rs — resolve_mention_pubkeys()

Act six · The acknowledgement
Wrong model "OK means it went out."

One guarantee. Five things you were hoping for.

OK
["OK", <id>, true, ""]
guaranteed

The relay accepted the operation through this path, and the durable write committed.

not guaranteed

Every subscriber received it.

not guaranteed

The audit row is committed — the dispatch returns after a bounded enqueue.

not guaranteed

Every projection is correct.

not guaranteed

A triggered workflow ran, or finished.

not guaranteed

Any external side effect happened.

01 Redis is fan-out. Postgres is history. A disconnected client must recover from history.

02 Slow consumers get disconnected. The socket is not a durable queue.

03 For anything critical: a consumer ledger on community + source_event_id + handler_version.

evidence crates/buzz-relay/src/handlers/event.rs — dispatch_persistent_event() spawns and returns

Act six · Turning the instrument around

Now you are adding a kind. Fill in the six gates.

01
What job is this? fact · document · command · private submission · signal · projection
02
Who signs it? human · agent · service · the relay itself
03
Which scope admits it? a new arm in required_scope_for_kind — or it is rejected
04
What is its scope of meaning? community-global · channel · recipient · author
05
What validates it beyond the signature? tags, referenced objects, membership, schema
06
Where does it land? events table · a dedicated table · nowhere
07
What is its lifecycle? immutable · replaceable · addressable · deletable · expiring
08
Which reads may expose it? history · live · COUNT · ids · search · export
09
Is its effect an invariant or a best-effort hook? this is the atomicity decision
10
What makes a retry safe? event id · domain key · revision · transaction boundary
11
What happens when an upstream client does not understand it? we are a fork; upstream will meet this kind

Eleven answers before a number. The number is the last decision, not the first.

Model · level 5
Act seven · back to the picture we started with

Four places we could put something of our own.

Box one
Someone signs a note
surface 1Run a service with its own key. It signs and reads notes like any other member. No relay change at all.
Box two
The server decides what it is
surface 2Add one entry to the list, so a new kind is accepted and gets a permission and a read rule.
Box three
Something happens
surface 3Give the kind its own handler — a transaction, a private table, enforcement before anyone sees it.
surface 4Answer a question the relay computes and signs on the spot, with nothing new stored.

The same three boxes we drew in the first five minutes. Every option we have is a position on this picture.

Act seven · Our fork

Four places we can extend. They are not equally expensive.

surface 1 · cheapest

External consumer

Do
authenticate a service as its own Nostr identity, subscribe, act, publish results as existing kinds
Get
zero relay change, zero upstream conflict, independent deploy cadence
Can't
enforce anything before publication · hide data · close a fan-out gap
surface 2 · small patch

New kind on the allowlist

Do
add to kind.rs, an arm in required_scope_for_kind, and a read-gate decision
Get
durable, typed, authorized, queryable storage with real privacy rules
Cost
a patch in two hot upstream files — mandatory special review on every sync
surface 3 · real patch

New handler or side effect

Do
route the kind to the command executor, a dedicated table, or a direct mutation
Get
transactional semantics, private storage, enforcement before disclosure
Cost
highest conflict surface — and you must answer question 09 honestly
surface 4 · read path

Relay-signed projection

Do
compute and sign a response at query time, as 39005 / 39006 / 40901 already do
Get
derived views with no new storage and no new write path
Cost
must be added to is_relay_only_kind so clients cannot forge it

The boundary rule: external service when it can safely consume accepted events and publish results. Patch the relay when correctness or confidentiality must be enforced before publication, storage, or disclosure.

governance UPSTREAM.md — kinds, auth and agent authorization changes require special review on every weekly sync

Act seven · Three proposals to argue about

Three candidates, deliberately at different costs.

Proposal 01 · cheapest real win

Wire the job vocabulary

43001–43006 already exist in the registry. Clients already render them from the activity feed. Scope::JobsRead and Scope::JobsWrite already exist in the auth crate and are used by no kind at all.

The only missing piece is one arm in required_scope_for_kind plus an authorization rule for who may accept, progress, and complete a job.

Surface 2 · ~1 file · read path already done
Do we want a first-class job protocol, or is this vocabulary a dead end we should stop rendering?
Proposal 02 · closes the Act 4 gap

A durable agent-run record

Three of four meanings of "running" vanish on restart. Proposal: the harness writes an addressable, author-only run record at turn start and turn end, keyed (agent, kind, scope), carrying an expiry the way push leases already do.

Then a relay-signed projection answers "every live run in this community" without exposing which worker did the work.

Surface 2 + 4 · new kind + query-time projection
Who signs the run record — the harness or the relay? And what does a crashed harness leave behind?
Proposal 03 · the hard one

Enforced compliance hold

An enterprise hold is an invariant, not a consequence: the event must not be considered committed unless the hold is recorded. That rules out a post-store side-effect hook.

The command executor is the right shape — but its own header warns that some domain mutations run on the pool outside the event-record transaction. So this needs a transactional outbox, not a spawned task.

Surface 3 · handler + transaction boundary
Are we willing to own a write-path patch in the file upstream changes most often?

evidence crates/buzz-auth/src/scope.rs — JobsRead/JobsWrite defined, unused · handlers/command_executor.rs — atomicity note

Act seven · How to choose

Cost is not effort. Cost is what we carry every Monday.

Proposal
Surface
Upstream sync cost
Blast radius if wrong
Must be true before we ship
01 · job vocabulary
allowlist arm
one hot file, additive
a kind nobody writes
who may accept / progress / complete
02 · agent-run record
new kind + projection
two files + read path
stale runs look live
expiry semantics and crash behaviour
03 · compliance hold
handler + transaction
write path — conflicts every sync
a hold that silently did not apply
a transactional outbox, not a spawn
Open question 01

How much of this belongs upstream?

Anything general — jobs, run observability — is cheaper contributed than carried. Anything enterprise-specific is ours forever.

A patch we carry is a patch we re-review weekly.
Open question 02

Do we add kinds, or add gates?

Several enterprise asks are authorization changes wearing a new-kind costume. Read gates are cheaper and safer than new vocabulary.

Prefer explicit schemas and authorization tests over many new numbers quickly.
Open question 03

What is our invariant list?

Question 09 on the worksheet, answered once, for the whole product: which effects must succeed before an event counts as committed?

Everything else can be a best-effort hook, and should be.
Where we started

Two events. One shape.

Buzz is not a Nostr message bus with features bolted on. It is a signed application protocol sitting on top of an authoritative state machine — and the envelope is the only thing its eight jobs have in common.

Take away 01

The kind number is the vocabulary. The handler is the machine.

Take away 02

A signature proves who supplied the data. It proves nothing about the claim.

Take away 03

Accepted is not delivered. Stored is not state.

And when you add the next one: creation, admission, transition, persistence, visibility, consequence. In that order.

Appendix · Reference

The 50 named kinds you cannot write.

31 · relay-authored

Projections & announcements

Membership
8000 8001 13534 44100 44101
Identity archive
8002 8003 13535
Group
39000 39001 39002 39003
Query-time
39005 39006 40901 40902 48104 30622
Other
40099 41001 48001 46001–46012
6 · read-only

Agent jobs

Kinds
43001 43002 43003 43004 43005 43006
Read by
buzz-db/src/store/feed.rs
Write arm
none
5 · ephemeral

Transient

Kinds
20001 presence · 20002 typing · 24134 pairing · 24200 observer · 24810 huddle burst
4 · auth

Other surfaces

Kinds
22242 NIP-42 · 24242 Blossom · 24243 identity binding · 27235 NIP-98
4 · dormant

No handler anywhere

Kinds
41 legacy metadata · 1063 file metadata · 9009 create invite · 49001 upload audit label

regenerate npm run verify — asserts this partition against crates/buzz-core/src/kind.rs

Appendix · Where the gates live

Which file owns which gate.

Gate 01 · creation
Builders, validation, desktop preparation and submission.
buzz-sdk/src/builders.rs · desktop/src-tauri/src/relay/submit.rs
Gate 02 · admission
The ten gates, the allowlist, the routing fork.
buzz-relay/src/handlers/ingest.rs
Gate 03 · transition
Commands, admin, moderation, reports, side effects.
handlers/{command_executor,relay_admin,moderation_commands,report,side_effects}.rs
Gate 04 · persistence
Event SQL, pagination, replacement and CAS.
buzz-db/src/store/{event,replaceable,workflow,feed}.rs
Gate 05 · visibility
Subscriptions, filters, the four read gates, the HTTP bridge.
handlers/req.rs · buzz-core/src/{filter,kind}.rs · api/bridge.rs
Gate 06 · consequence
Dispatch, fan-out, workflow matching and execution, agent runtime.
handlers/event.rs · workflow_sink.rs · buzz-workflow/* · buzz-acp/*