v0.0.55 - Defer trigger loading until self-skill EOSE and add trigger subscription debug instrumentation

This commit is contained in:
Your Name
2026-03-09 16:17:16 -04:00
parent 4f35fd3888
commit 22b2064fbb
7 changed files with 243 additions and 68 deletions
+2 -2
View File
@@ -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.54
## Current Status — v0.0.55
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.54Add cheerleader triggered skill to active config with valid admin filter and adoption entry
> Last release update: v0.0.55Defer trigger loading until self-skill EOSE and add trigger subscription debug instrumentation
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+38 -3
View File
@@ -20,6 +20,37 @@
static volatile sig_atomic_t g_running = 1;
typedef struct {
trigger_manager_t* trigger_manager;
int loaded_once;
} deferred_trigger_load_ctx_t;
static void on_self_skill_eose_load_triggers(int event_count, void* user_data) {
deferred_trigger_load_ctx_t* ctx = (deferred_trigger_load_ctx_t*)user_data;
if (!ctx || !ctx->trigger_manager) {
return;
}
if (ctx->loaded_once) {
DEBUG_TRACE("[didactyl] deferred trigger load skipped (already loaded once) event_count=%d", event_count);
return;
}
DEBUG_INFO("[didactyl] deferred trigger load begin after self-skill EOSE (event_count=%d)", event_count);
if (trigger_manager_load_from_skills(ctx->trigger_manager) != 0) {
DEBUG_WARN("[didactyl] deferred trigger load failed after self-skill EOSE");
return;
}
ctx->loaded_once = 1;
char* status = trigger_manager_status_json(ctx->trigger_manager);
if (status) {
DEBUG_INFO("[didactyl] deferred trigger load status: %s", status);
free(status);
}
}
static void signal_handler(int signum) {
(void)signum;
g_running = 0;
@@ -232,9 +263,13 @@ int main(int argc, char** argv) {
}
agent_set_trigger_manager(&trigger_manager);
if (trigger_manager_load_from_skills(&trigger_manager) != 0) {
DEBUG_WARN("[didactyl] startup phase: trigger manager load from skills failed (continuing)");
}
deferred_trigger_load_ctx_t deferred_trigger_ctx = {
.trigger_manager = &trigger_manager,
.loaded_once = 0
};
nostr_handler_set_self_skill_eose_callback(on_self_skill_eose_load_triggers, &deferred_trigger_ctx);
DEBUG_INFO("[didactyl] startup phase: deferred trigger load will run after self-skill EOSE");
DEBUG_INFO("[didactyl] startup phase: subscribe admin context begin");
if (nostr_handler_subscribe_admin_context() != 0) {
+2 -2
View File
@@ -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 54
#define DIDACTYL_VERSION "v0.0.54"
#define DIDACTYL_VERSION_PATCH 55
#define DIDACTYL_VERSION "v0.0.55"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+55 -1
View File
@@ -48,6 +48,9 @@ static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
#define DM_DEDUP_CACHE_SIZE 256
static nostr_self_skill_eose_cb_t g_self_skill_eose_cb = NULL;
static void* g_self_skill_eose_user_data = NULL;
static char g_seen_dm_ids[DM_DEDUP_CACHE_SIZE][65];
static int g_seen_dm_count = 0;
static int g_seen_dm_next = 0;
@@ -942,6 +945,16 @@ static void on_eose(cJSON** events, int event_count, void* user_data) {
DEBUG_TRACE("[didactyl] DEBUG on_eose called: event_count=%d", event_count);
}
static void on_self_skill_eose(cJSON** events, int event_count, void* user_data) {
(void)events;
(void)user_data;
DEBUG_INFO("[didactyl] self-skill EOSE received: cached skill events=%d", event_count);
if (g_self_skill_eose_cb) {
g_self_skill_eose_cb(event_count, g_self_skill_eose_user_data);
}
}
static void free_admin_context_locked(void) {
free(g_admin_kind0_json);
g_admin_kind0_json = NULL;
@@ -1349,7 +1362,7 @@ int nostr_handler_subscribe_self_skills(void) {
g_cfg->relay_count,
filter,
on_self_skill_event,
on_eose,
on_self_skill_eose,
NULL,
0,
0,
@@ -1493,6 +1506,11 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
return 0;
}
void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data) {
g_self_skill_eose_cb = callback;
g_self_skill_eose_user_data = user_data;
}
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) {
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) {
return -1;
@@ -1755,6 +1773,42 @@ char* nostr_handler_query_json(cJSON* filter, int timeout_ms) {
return out;
}
nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(cJSON** events, int event_count, void* user_data),
void* user_data,
int close_on_eose,
int enable_deduplication,
nostr_pool_eose_result_mode_t result_mode,
int relay_timeout_seconds,
int eose_timeout_seconds) {
if (!g_cfg || !g_pool || !filter || !on_event) {
return NULL;
}
return nostr_relay_pool_subscribe(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
filter,
on_event,
on_eose,
user_data,
close_on_eose,
enable_deduplication,
result_mode,
relay_timeout_seconds,
eose_timeout_seconds);
}
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription) {
if (!subscription) {
return 0;
}
return nostr_pool_subscription_close(subscription);
}
static void publish_pending_startup_events_for_relay_index(int relay_index, const char* reason) {
if (!g_cfg || !g_pool || !g_startup_publish_tracking_enabled || !g_startup_published) {
return;
+15
View File
@@ -3,6 +3,7 @@
#include "config.h"
#include "cjson/cJSON.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
typedef enum {
DIDACTYL_SENDER_ADMIN = 1,
@@ -27,16 +28,30 @@ typedef struct {
char** relays;
} nostr_publish_result_t;
typedef void (*nostr_self_skill_eose_cb_t)(int event_count, void* user_data);
int nostr_handler_init(didactyl_config_t* config);
int nostr_handler_subscribe_admin_context(void);
int nostr_handler_subscribe_self_skills(void);
char* nostr_handler_get_self_events_by_kind_json(int kind);
void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data);
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data);
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message);
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message);
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result);
void nostr_handler_publish_result_free(nostr_publish_result_t* result);
char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(cJSON** events, int event_count, void* user_data),
void* user_data,
int close_on_eose,
int enable_deduplication,
nostr_pool_eose_result_mode_t result_mode,
int relay_timeout_seconds,
int eose_timeout_seconds);
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription);
int nostr_handler_poll(int timeout_ms);
int nostr_handler_reconcile_startup_events(void);
void nostr_handler_refresh_relay_statuses(void);
+128 -60
View File
@@ -12,6 +12,11 @@
#include "debug.h"
#include "nostr_handler.h"
typedef struct {
trigger_manager_t* mgr;
char skill_slug[TRIGGER_SKILL_SLUG_MAX];
} trigger_subscription_ctx_t;
static int clamp_enabled(int enabled) {
return enabled ? 1 : 0;
}
@@ -220,6 +225,107 @@ static int maybe_fire_trigger_locked(trigger_manager_t* mgr, int index, cJSON* e
return 1;
}
static void on_trigger_subscription_eose(cJSON** events, int event_count, void* user_data) {
(void)events;
(void)event_count;
(void)user_data;
}
static void close_trigger_subscription_locked(active_trigger_t* t) {
if (!t) {
return;
}
if (t->subscription) {
(void)nostr_handler_close_subscription(t->subscription);
t->subscription = NULL;
}
if (t->subscription_ctx) {
free(t->subscription_ctx);
t->subscription_ctx = NULL;
}
}
static void on_trigger_subscription_event(cJSON* event, const char* relay_url, void* user_data) {
trigger_subscription_ctx_t* ctx = (trigger_subscription_ctx_t*)user_data;
if (!ctx || !ctx->mgr || !event) {
return;
}
trigger_manager_t* mgr = ctx->mgr;
pthread_mutex_lock(&mgr->mutex);
int idx = find_trigger_index_locked(mgr, ctx->skill_slug);
if (idx >= 0) {
(void)maybe_fire_trigger_locked(mgr, idx, event, relay_url);
}
pthread_mutex_unlock(&mgr->mutex);
}
static int register_trigger_subscription_locked(trigger_manager_t* mgr, active_trigger_t* t) {
if (!mgr || !t) {
return -1;
}
close_trigger_subscription_locked(t);
if (!t->enabled || t->filter_json[0] == '\0') {
return 0;
}
cJSON* filter = cJSON_Parse(t->filter_json);
if (!filter || !cJSON_IsObject(filter)) {
cJSON_Delete(filter);
return -1;
}
cJSON* since = cJSON_GetObjectItemCaseSensitive(filter, "since");
if (!since || !cJSON_IsNumber(since)) {
time_t now = time(NULL);
time_t default_since = now > 30 ? now - 30 : 0;
cJSON_AddNumberToObject(filter, "since", (double)default_since);
}
cJSON* limit = cJSON_GetObjectItemCaseSensitive(filter, "limit");
if (!limit || !cJSON_IsNumber(limit)) {
cJSON_AddNumberToObject(filter, "limit", 200);
}
trigger_subscription_ctx_t* ctx = (trigger_subscription_ctx_t*)calloc(1, sizeof(*ctx));
if (!ctx) {
cJSON_Delete(filter);
return -1;
}
ctx->mgr = mgr;
snprintf(ctx->skill_slug, sizeof(ctx->skill_slug), "%s", t->skill_slug);
nostr_pool_subscription_t* sub = nostr_handler_subscribe_with_filter(
filter,
on_trigger_subscription_event,
on_trigger_subscription_eose,
ctx,
0,
1,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
cJSON_Delete(filter);
if (!sub) {
free(ctx);
return -1;
}
t->subscription = sub;
t->subscription_ctx = ctx;
return 0;
}
int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg) {
if (!mgr || !cfg) {
return -1;
@@ -433,6 +539,15 @@ int trigger_manager_add(trigger_manager_t* mgr,
t->enabled = clamp_enabled(enabled);
t->last_fired = 0;
t->last_seen_created_at = 0;
t->subscription = NULL;
t->subscription_ctx = NULL;
if (register_trigger_subscription_locked(mgr, t) != 0) {
pthread_mutex_unlock(&mgr->mutex);
DEBUG_WARN("[didactyl] trigger add rejected: failed to create subscription slug=%s", skill_slug);
memset(t, 0, sizeof(*t));
return -1;
}
mgr->count++;
@@ -455,6 +570,8 @@ int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_slug) {
return 0;
}
close_trigger_subscription_locked(&mgr->triggers[idx]);
if (idx < mgr->count - 1) {
memmove(&mgr->triggers[idx],
&mgr->triggers[idx + 1],
@@ -498,6 +615,12 @@ int trigger_manager_update(trigger_manager_t* mgr,
t->action_type = action_type;
t->enabled = clamp_enabled(enabled);
if (register_trigger_subscription_locked(mgr, t) != 0) {
pthread_mutex_unlock(&mgr->mutex);
DEBUG_WARN("[didactyl] trigger update failed to (re)subscribe slug=%s", skill_slug);
return -1;
}
pthread_mutex_unlock(&mgr->mutex);
DEBUG_INFO("[didactyl] trigger updated slug=%s action=%d enabled=%d", skill_slug, (int)action_type, clamp_enabled(enabled));
@@ -522,66 +645,7 @@ int trigger_manager_active_count(trigger_manager_t* mgr) {
}
int trigger_manager_poll(trigger_manager_t* mgr) {
if (!mgr || !mgr->cfg || !mgr->cfg->triggers.enabled) {
return 0;
}
time_t now = time(NULL);
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 10) {
return 0;
}
mgr->last_poll_at = now;
pthread_mutex_lock(&mgr->mutex);
int count = mgr->count;
for (int i = 0; i < count; i++) {
active_trigger_t snapshot = mgr->triggers[i];
if (!snapshot.enabled || snapshot.filter_json[0] == '\0') {
continue;
}
cJSON* filter = cJSON_Parse(snapshot.filter_json);
if (!filter || !cJSON_IsObject(filter)) {
cJSON_Delete(filter);
continue;
}
time_t since = snapshot.last_seen_created_at > 0 ? snapshot.last_seen_created_at + 1 : now - 30;
if (since < 0) since = 0;
cJSON_AddNumberToObject(filter, "since", (double)since);
cJSON_AddNumberToObject(filter, "limit", 8);
char* events_json = nostr_handler_query_json(filter, 1200);
cJSON_Delete(filter);
if (!events_json) {
continue;
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
continue;
}
int n = cJSON_GetArraySize(events);
for (int e = 0; e < n; e++) {
cJSON* ev = cJSON_GetArrayItem(events, e);
if (!ev || !cJSON_IsObject(ev)) continue;
int idx = find_trigger_index_locked(mgr, snapshot.skill_slug);
if (idx < 0) {
continue;
}
(void)maybe_fire_trigger_locked(mgr, idx, ev, NULL);
}
cJSON_Delete(events);
}
pthread_mutex_unlock(&mgr->mutex);
(void)mgr;
return 0;
}
@@ -627,6 +691,7 @@ char* trigger_manager_status_json(trigger_manager_t* mgr) {
cJSON_AddStringToObject(item, "filter_json", t->filter_json);
cJSON_AddStringToObject(item, "action", t->action_type == TRIGGER_ACTION_TEMPLATE ? "template" : "llm");
cJSON_AddBoolToObject(item, "enabled", t->enabled ? 1 : 0);
cJSON_AddBoolToObject(item, "subscribed", t->subscription ? 1 : 0);
cJSON_AddNumberToObject(item, "last_fired", (double)t->last_fired);
cJSON_AddNumberToObject(item, "last_seen_created_at", (double)t->last_seen_created_at);
cJSON_AddItemToArray(arr, item);
@@ -648,6 +713,9 @@ void trigger_manager_cleanup(trigger_manager_t* mgr) {
}
pthread_mutex_lock(&mgr->mutex);
for (int i = 0; i < mgr->count; i++) {
close_trigger_subscription_locked(&mgr->triggers[i]);
}
free(mgr->triggers);
mgr->triggers = NULL;
mgr->count = 0;
+3
View File
@@ -5,6 +5,7 @@
#include <time.h>
#include "config.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#define TRIGGER_DEFAULT_MAX_ACTIVE 16
#define TRIGGER_SKILL_SLUG_MAX 65
@@ -24,6 +25,8 @@ typedef struct {
int enabled;
time_t last_fired;
time_t last_seen_created_at;
nostr_pool_subscription_t* subscription;
void* subscription_ctx;
} active_trigger_t;
typedef struct trigger_manager {