From f7dd1f98c64abd65998492dcabccc5d1857c2ead Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 9 Mar 2026 18:14:33 -0400 Subject: [PATCH] v0.0.58 - Disable pool dedup on trigger subscriptions to restore kind-1 firing --- README.md | 4 +- context.log.md | 56 ++++++++++++ docs/SUBSCRIPTIONS.md | 194 ++++++++++++++++++++++++++++++++++++++++++ src/main.h | 4 +- src/trigger_manager.c | 2 +- 5 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 docs/SUBSCRIPTIONS.md diff --git a/README.md b/README.md index fd98dd6..83180c1 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,11 @@ Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fa Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.0.57 +## Current Status — v0.0.58 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.0.57 — Register triggers live from self-skill websocket events +> Last release update: v0.0.58 — Disable pool dedup on trigger subscriptions to restore kind-1 firing - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/context.log.md b/context.log.md index 5576c2f..fd5d1d0 100644 --- a/context.log.md +++ b/context.log.md @@ -1,3 +1,59 @@ +```text +Context Log - not seen by model +timestamp=2026-03-09 17:21:46 +phase=direct_tool_exec +sender=8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e +model=claude-opus-4.6 +context_bytes=683 +approx_tokens=170 +``` + +slash=/trigger_list +result_json={"success":true,"count":1,"active":1,"triggers":[{"skill_slug":"cheerleader","filter_json":"{\"kinds\":[1],\"authors\":[\"8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e\"]}","action":"llm","enabled":true,"subscribed":true,"last_fired":0,"last_seen_created_at":0}]} +result_markdown=- **success:** true +- **count:** 1 +- **active:** 1 +- **triggers:** + - + - **skill_slug:** cheerleader + - **filter_json:** {"kinds":[1],"authors":["8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"]} + - **action:** llm + - **enabled:** true + - **subscribed:** true + - **last_fired:** 0 + - **last_seen_created_at:** 0 + + +--- + +```text +Context Log - not seen by model +timestamp=2026-03-09 17:20:38 +phase=direct_tool_exec +sender=8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e +model=claude-opus-4.6 +context_bytes=683 +approx_tokens=170 +``` + +slash=/trigger_list +result_json={"success":true,"count":1,"active":1,"triggers":[{"skill_slug":"cheerleader","filter_json":"{\"kinds\":[1],\"authors\":[\"8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e\"]}","action":"llm","enabled":true,"subscribed":true,"last_fired":0,"last_seen_created_at":0}]} +result_markdown=- **success:** true +- **count:** 1 +- **active:** 1 +- **triggers:** + - + - **skill_slug:** cheerleader + - **filter_json:** {"kinds":[1],"authors":["8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"]} + - **action:** llm + - **enabled:** true + - **subscribed:** true + - **last_fired:** 0 + - **last_seen_created_at:** 0 + + +--- + ```text Context Log - not seen by model timestamp=2026-03-09 16:30:42 diff --git a/docs/SUBSCRIPTIONS.md b/docs/SUBSCRIPTIONS.md new file mode 100644 index 0000000..d8328ef --- /dev/null +++ b/docs/SUBSCRIPTIONS.md @@ -0,0 +1,194 @@ +# 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 diff --git a/src/main.h b/src/main.h index acd978a..f8feba7 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 0 -#define DIDACTYL_VERSION_PATCH 57 -#define DIDACTYL_VERSION "v0.0.57" +#define DIDACTYL_VERSION_PATCH 58 +#define DIDACTYL_VERSION "v0.0.58" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/trigger_manager.c b/src/trigger_manager.c index 75a0c1f..f6eccba 100644 --- a/src/trigger_manager.c +++ b/src/trigger_manager.c @@ -308,7 +308,7 @@ static int register_trigger_subscription_locked(trigger_manager_t* mgr, active_t on_trigger_subscription_eose, ctx, 0, - 1, + 0, NOSTR_POOL_EOSE_FULL_SET, 30, 120);