195 lines
8.0 KiB
Markdown
195 lines
8.0 KiB
Markdown
# Didactyl — Nostr Subscriptions
|
|
|
|
## Overview
|
|
|
|
Didactyl maintains persistent websocket subscriptions to Nostr relays for the lifetime of the process. Subscriptions are opened during startup and are **never closed** — the relay pool keeps them alive, automatically reconnecting and resubscribing when relays drop.
|
|
|
|
All subscriptions are created via `nostr_relay_pool_subscribe()` from `nostr_core_lib` and are sent to every relay in the configured relay list.
|
|
|
|
## Startup Sequence
|
|
|
|
The subscriptions are opened in a specific order during `main()` startup. The diagram below shows the full sequence:
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[nostr_handler_init] --> B[Connect to all relays]
|
|
B --> C[nostr_handler_reconcile_startup_events]
|
|
C --> D[Publish startup events to relays]
|
|
D --> E[trigger_manager_init]
|
|
E --> F[trigger_manager_load_from_startup_events]
|
|
F --> G["Subscribe: Admin Context"]
|
|
G --> H["Subscribe: Self Skills"]
|
|
H --> I[Send startup DM to admin]
|
|
I --> J["Subscribe: DMs"]
|
|
J --> K[Enter main poll loop]
|
|
K --> L["Poll: nostr_handler_poll + trigger_manager_poll + http_api_poll"]
|
|
|
|
style F fill:#2a7,stroke:#333,color:#fff
|
|
style G fill:#27a,stroke:#333,color:#fff
|
|
style H fill:#27a,stroke:#333,color:#fff
|
|
style J fill:#27a,stroke:#333,color:#fff
|
|
```
|
|
|
|
## Subscription Categories
|
|
|
|
### 1. Admin Context Subscription
|
|
|
|
**Function:** `nostr_handler_subscribe_admin_context()` in `src/nostr_handler.c`
|
|
**When:** During startup, before self-skill subscription
|
|
**Condition:** Only if `admin_context.enabled` is true in config
|
|
|
|
This creates up to two persistent subscriptions for the admin's pubkey:
|
|
|
|
#### Profile Subscription
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Kinds** | 0 (profile), 3 (contacts), 10002 (relay list) — each configurable |
|
|
| **Authors** | Admin pubkey |
|
|
| **Limit** | 32 |
|
|
| **Callback** | `on_admin_context_event` |
|
|
| **Dedup** | Enabled |
|
|
| **Close on EOSE** | No |
|
|
|
|
Tracks the admin's profile metadata, contact list (WoT), and relay preferences. Used to build agent context about who the admin is.
|
|
|
|
#### Kind 1 Notes Subscription
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Kinds** | 1 |
|
|
| **Authors** | Admin pubkey |
|
|
| **Limit** | Configurable via `kind_1_limit`, default 10, max 256 |
|
|
| **Callback** | `on_admin_context_event` |
|
|
| **Dedup** | Enabled |
|
|
| **Close on EOSE** | No |
|
|
| **Condition** | Only if `admin_context.track_kind_1` is true |
|
|
|
|
Tracks the admin's recent public notes. Used for agent context and as the event source for triggered skills that watch admin posts.
|
|
|
|
### 2. Self-Skill Subscription
|
|
|
|
**Function:** `nostr_handler_subscribe_self_skills()` in `src/nostr_handler.c`
|
|
**When:** During startup, after admin context subscription
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Kinds** | 31123 (public skill), 31124 (private skill), 10123 (adoption list) |
|
|
| **Authors** | Agent's own pubkey |
|
|
| **Limit** | 300 |
|
|
| **Callback** | `on_self_skill_event` |
|
|
| **EOSE Callback** | `on_self_skill_eose` |
|
|
| **Dedup** | Disabled (handles dedup internally via cache upsert) |
|
|
| **Close on EOSE** | No |
|
|
|
|
This is the core skill awareness subscription. It serves three purposes:
|
|
|
|
1. **Cache population** — Every arriving event is stored in the in-memory self-skill cache via `self_skill_cache_upsert_event_locked()`, making skills available for LLM tool calls and context building.
|
|
|
|
2. **Live trigger registration** — When a kind 31123 or 31124 event arrives with `trigger=nostr-subscription` and a valid `filter` tag, `register_trigger_from_self_skill_event()` immediately calls `trigger_manager_add()` to create a persistent trigger subscription. This means skills published from any client are automatically activated without restart.
|
|
|
|
3. **Deferred bulk load** — After EOSE, the `on_self_skill_eose` callback fires `trigger_manager_load_from_skills()` as a one-time bulk scan of the adoption list. This catches any skills that were already cached before the per-event path was wired up.
|
|
|
|
### 3. DM Subscriptions
|
|
|
|
**Function:** `nostr_handler_subscribe_dms()` in `src/nostr_handler.c`
|
|
**When:** During startup, after self-skill subscription and startup DM
|
|
**Required:** Yes — startup fails if DM subscription cannot be created
|
|
|
|
Creates one or two subscriptions depending on the configured DM protocol:
|
|
|
|
#### NIP-04 DM Subscription
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Kinds** | 4 |
|
|
| **#p** | Agent's own pubkey |
|
|
| **Since** | Process start time |
|
|
| **Limit** | 100 |
|
|
| **Callback** | `on_event` (routes to `agent_on_message`) |
|
|
| **Dedup** | Disabled (handled by `dm_id_seen_or_remember`) |
|
|
| **Close on EOSE** | No |
|
|
| **Condition** | `dm_protocol` is `nip04` or `both` |
|
|
|
|
#### NIP-17 DM Subscription
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Kinds** | 1059 (gift wrap) |
|
|
| **#p** | Agent's own pubkey |
|
|
| **Since** | Process start time |
|
|
| **Limit** | 400 |
|
|
| **Callback** | `on_event` (unwraps gift wrap, routes to `agent_on_message`) |
|
|
| **Dedup** | Disabled (handled by `dm_id_seen_or_remember`) |
|
|
| **Close on EOSE** | No |
|
|
| **Condition** | `dm_protocol` is `nip17` or `both` |
|
|
|
|
### 4. Trigger Subscriptions
|
|
|
|
**Function:** `register_trigger_subscription_locked()` in `src/trigger_manager.c`
|
|
**When:** Dynamically, whenever a trigger is registered via `trigger_manager_add()`
|
|
**Created by:** `nostr_handler_subscribe_with_filter()` wrapper
|
|
|
|
Each active trigger gets its own persistent subscription based on the skill's `filter` tag:
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Filter** | Parsed from the skill's `filter` tag JSON |
|
|
| **Since** | From filter, or defaults to `now - 30s` |
|
|
| **Limit** | From filter, or defaults to 200 |
|
|
| **Callback** | `on_trigger_subscription_event` |
|
|
| **Dedup** | Enabled |
|
|
| **Close on EOSE** | No |
|
|
|
|
When an event matches the filter, `maybe_fire_trigger_locked()` checks cooldown and dedup, then executes the trigger action (LLM or template).
|
|
|
|
Trigger subscriptions are created at three points:
|
|
- **Startup config scan** — `trigger_manager_load_from_startup_events()` parses `startup_events[]` from config for skills with trigger tags
|
|
- **Live self-skill event** — `register_trigger_from_self_skill_event()` in the self-skill subscription callback
|
|
- **EOSE bulk load** — `trigger_manager_load_from_skills()` after self-skill EOSE
|
|
- **Runtime tool call** — `skill_create` tool with trigger parameters
|
|
|
|
## Subscription Parameters
|
|
|
|
All subscriptions share these common pool parameters:
|
|
|
|
| Parameter | Value | Meaning |
|
|
|-----------|-------|---------|
|
|
| `close_on_eose` | 0 | Subscription stays open after initial EOSE |
|
|
| `result_mode` | `NOSTR_POOL_EOSE_FULL_SET` | EOSE fires after all relays respond or timeout |
|
|
| `relay_timeout_seconds` | 30 | Per-relay timeout for initial response |
|
|
| `eose_timeout_seconds` | 120 | Overall EOSE timeout across all relays |
|
|
|
|
## Subscription Lifecycle
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
INIT["Process Start"] --> CONNECT["Connect Relays"]
|
|
CONNECT --> SUB["Open Subscriptions"]
|
|
SUB --> LIVE["Live Event Stream"]
|
|
LIVE --> |"Relay disconnects"| RECON["Auto-Reconnect"]
|
|
RECON --> |"Relay reconnects"| RESUB["Auto-Resubscribe"]
|
|
RESUB --> LIVE
|
|
LIVE --> |"SIGINT/SIGTERM"| SHUT["Shutdown"]
|
|
SHUT --> CLOSE["Close All + Cleanup"]
|
|
```
|
|
|
|
Subscriptions are never manually closed during normal operation. The relay pool handles reconnection and resubscription transparently. On shutdown, `trigger_manager_cleanup()` closes trigger subscriptions and `nostr_handler_cleanup()` destroys the pool.
|
|
|
|
## Summary Table
|
|
|
|
| Subscription | Kinds | Target | Persistent | Created At |
|
|
|-------------|-------|--------|-----------|------------|
|
|
| Admin Profile | 0, 3, 10002 | Admin pubkey | Yes | Startup |
|
|
| Admin Notes | 1 | Admin pubkey | Yes | Startup |
|
|
| Self Skills | 31123, 31124, 10123 | Own pubkey | Yes | Startup |
|
|
| DMs NIP-04 | 4 | Own pubkey (#p) | Yes | Startup |
|
|
| DMs NIP-17 | 1059 | Own pubkey (#p) | Yes | Startup |
|
|
| Trigger N | Per skill filter | Varies | Yes | Dynamic |
|
|
|
|
## Related Documentation
|
|
|
|
- [Skills](SKILLS.md) — Skill event format and trigger tags
|
|
- [Tools](TOOLS.md) — `skill_create` tool with trigger parameters
|
|
- [API](API.md) — `trigger_list` and `trigger_status` endpoints
|