From 975acc2bf48ddbd98d58864ba04f95b2fcca6803 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sun, 8 Mar 2026 09:58:35 -0700 Subject: Add ephemeral event support (kinds 20000-29999) Per NIP-01, ephemeral events are broadcast to subscribers but not persisted to storage. This enables real-time features like typing indicators without bloating the database. Also adds typing-indicators.md spec for kind 20001. --- docs/typing-indicators.md | 80 ++++++++++++++++++++++++++++++++++ internal/handler/websocket/handler.go | 15 +++++++ muxstr | Bin 0 -> 27788215 bytes 3 files changed, 95 insertions(+) create mode 100644 docs/typing-indicators.md create mode 100755 muxstr diff --git a/docs/typing-indicators.md b/docs/typing-indicators.md new file mode 100644 index 0000000..a85633d --- /dev/null +++ b/docs/typing-indicators.md @@ -0,0 +1,80 @@ +# Typing/Thinking Indicators + +Ephemeral events for real-time activity feedback in Nostr conversations. + +## Event Kind + +**Kind 20001** - Typing/Thinking Indicator (ephemeral) + +Per NIP-01, kinds 20000-29999 are ephemeral and should not be stored by relays. + +## Event Structure + +```json +{ + "kind": 20001, + "content": "", + "tags": [ + ["p", ""], + ["e", "", "", "reply"] + ], + "created_at": , + "pubkey": "", + "id": "", + "sig": "" +} +``` + +### Content Values + +| Value | Meaning | +|-------|---------| +| `typing` | User is composing a message | +| `thinking` | Agent is processing/generating | +| `stopped` | Activity stopped (explicit clear) | +| `` (empty) | Clear indicator | + +### Tags + +- `p` (required): Pubkey of the recipient +- `e` (optional): Event being replied to, with `reply` marker + +## Client Behavior + +### Sending +1. Publish `thinking` when starting to process a message +2. Publish actual response when done +3. Optionally publish `stopped` or empty content to clear early + +### Receiving +1. Subscribe to kind 20001 events where you're tagged +2. Display indicator while active +3. Auto-clear after ~15 seconds if no update (stale indicator) +4. Clear immediately when: + - Receive `stopped` or empty content from same pubkey + - Receive an actual message (kind 1, 4, 14, etc.) from same pubkey + +## Example Flow + +``` +Agent receives message + └─> Publish kind:20001 content:"thinking" p:[user] + └─> Process request... + └─> Publish kind:1 content:"Here's the answer..." (reply) + └─> (indicator auto-clears on client when they see the kind:1) +``` + +## Relay Support + +Relays SHOULD: +- Forward ephemeral events to matching subscriptions +- NOT persist ephemeral events to storage +- NOT return ephemeral events in REQ responses for historical data + +muxstr handles this correctly as of the ephemeral event support addition. + +## Security Considerations + +- Indicators reveal that someone is actively engaged with you +- Consider privacy implications before implementing +- Clients MAY offer a setting to disable sending indicators diff --git a/internal/handler/websocket/handler.go b/internal/handler/websocket/handler.go index c64f3f9..b591f61 100644 --- a/internal/handler/websocket/handler.go +++ b/internal/handler/websocket/handler.go @@ -359,6 +359,14 @@ func (h *Handler) handleEvent(ctx context.Context, conn *websocket.Conn, raw []j return nil } + // Handle ephemeral events (kinds 20000-29999) - broadcast but don't store + if isEphemeralKind(pbEvent.Kind) { + h.subs.MatchAndFan(pbEvent) + status = "ok" + h.sendOK(ctx, conn, event.ID, true, "") + return nil + } + eventData := &storage.EventData{ Event: pbEvent, CanonicalJSON: canonicalJSON, @@ -643,3 +651,10 @@ func (h *Handler) isAllowedKind(kind int32) bool { } return h.allowedKinds[kind] } + +// isEphemeralKind returns true if the event kind is ephemeral (20000-29999). +// Ephemeral events are broadcast to subscribers but not stored. +// See NIP-01: https://github.com/nostr-protocol/nips/blob/master/01.md +func isEphemeralKind(kind int32) bool { + return kind >= 20000 && kind < 30000 +} diff --git a/muxstr b/muxstr new file mode 100755 index 0000000..6d32892 Binary files /dev/null and b/muxstr differ -- cgit v1.2.3