v0.0.55 - Defer trigger loading until self-skill EOSE and add trigger subscription debug instrumentation
This commit is contained in:
@@ -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.54 — Add cheerleader triggered skill to active config with valid admin filter and adoption entry
|
||||
> Last release update: v0.0.55 — Defer 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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user