#define _POSIX_C_SOURCE 200809L #include "trigger_manager.h" #include #include #include #include #include "agent.h" #include "cjson/cJSON.h" #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; } static int ensure_capacity(trigger_manager_t* mgr, int needed) { if (!mgr || needed <= 0) { return -1; } if (mgr->capacity >= needed) { return 0; } int new_cap = mgr->capacity > 0 ? mgr->capacity : TRIGGER_DEFAULT_MAX_ACTIVE; while (new_cap < needed) { if (new_cap > 1024) { return -1; } new_cap *= 2; } active_trigger_t* grown = (active_trigger_t*)realloc(mgr->triggers, (size_t)new_cap * sizeof(active_trigger_t)); if (!grown) { return -1; } if (new_cap > mgr->capacity) { memset(&grown[mgr->capacity], 0, (size_t)(new_cap - mgr->capacity) * sizeof(active_trigger_t)); } mgr->triggers = grown; mgr->capacity = new_cap; return 0; } static int find_trigger_index_locked(trigger_manager_t* mgr, const char* skill_slug) { if (!mgr || !skill_slug || skill_slug[0] == '\0') { return -1; } for (int i = 0; i < mgr->count; i++) { if (strncmp(mgr->triggers[i].skill_slug, skill_slug, TRIGGER_SKILL_SLUG_MAX) == 0) { return i; } } return -1; } static cJSON* find_tag_value_string(cJSON* tags, const char* key) { if (!tags || !key || !cJSON_IsArray(tags)) return NULL; int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue; cJSON* k = cJSON_GetArrayItem(tag, 0); cJSON* v = cJSON_GetArrayItem(tag, 1); if (k && v && cJSON_IsString(k) && cJSON_IsString(v) && k->valuestring && v->valuestring && strcmp(k->valuestring, key) == 0) { return v; } } return NULL; } static int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_slug[65]) { if (!addr || !out_kind || !out_pubkey || !out_slug) { return -1; } const char* p1 = strchr(addr, ':'); if (!p1) return -1; const char* p2 = strchr(p1 + 1, ':'); if (!p2) return -1; char kind_buf[16] = {0}; size_t kind_len = (size_t)(p1 - addr); if (kind_len == 0 || kind_len >= sizeof(kind_buf)) return -1; memcpy(kind_buf, addr, kind_len); int kind = atoi(kind_buf); if (kind != 31123 && kind != 31124) return -1; size_t pub_len = (size_t)(p2 - (p1 + 1)); if (pub_len != 64U) return -1; memcpy(out_pubkey, p1 + 1, 64U); out_pubkey[64] = '\0'; size_t slug_len = strlen(p2 + 1); if (slug_len == 0 || slug_len >= 65U) return -1; memcpy(out_slug, p2 + 1, slug_len + 1U); *out_kind = kind; return 0; } static char* build_template_output(const active_trigger_t* t, cJSON* event, const char* relay_url) { (void)relay_url; if (!t || !event) return NULL; cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id"); cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : ""; const char* pubkey_s = (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring) ? pubkey->valuestring : "unknown"; const char* id_s = (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : ""; int kind_i = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1; const char* tmpl = t->skill_content; if (!tmpl || tmpl[0] == '\0') { return NULL; } char out[TRIGGER_SKILL_CONTENT_MAX + 256]; snprintf(out, sizeof(out), "%s\n\n[event id=%s kind=%d pubkey=%s]\n%s", tmpl, id_s, kind_i, pubkey_s, content_s); return strdup(out); } static void execute_template_action(trigger_manager_t* mgr, const active_trigger_t* t, cJSON* event, const char* relay_url) { if (!mgr || !mgr->cfg || !t || !event) { return; } char* rendered = build_template_output(t, event, relay_url); if (!rendered) { return; } if (strncmp(rendered, "DM admin:", 9) == 0) { const char* body = rendered + 9; while (*body == ' ') body++; (void)nostr_handler_send_dm_auto(mgr->cfg->admin.pubkey, body); } else if (strncmp(rendered, "POST:", 5) == 0) { const char* body = rendered + 5; while (*body == ' ') body++; (void)nostr_handler_publish_kind_event(1, body, NULL, NULL); } else if (strncmp(rendered, "LOG:", 4) == 0) { const char* body = rendered + 4; while (*body == ' ') body++; DEBUG_INFO("[didactyl] trigger template log (%s): %s", t->skill_slug, body); } else { (void)nostr_handler_send_dm_auto(mgr->cfg->admin.pubkey, rendered); } free(rendered); } static void execute_llm_action(const active_trigger_t* t, cJSON* event, const char* relay_url) { if (!t || !event) { return; } agent_on_trigger(t->skill_slug, t->skill_content, event, relay_url); } static int maybe_fire_trigger_locked(trigger_manager_t* mgr, int index, cJSON* event, const char* relay_url) { active_trigger_t* t = &mgr->triggers[index]; if (!t->enabled) { return 0; } cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); time_t created_ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : 0; if (created_ts > 0 && created_ts <= t->last_seen_created_at) { return 0; } time_t now = time(NULL); int cooldown = mgr->cfg->triggers.cooldown_seconds; if (cooldown < 0) cooldown = 0; if (cooldown > 0 && t->last_fired > 0 && (now - t->last_fired) < cooldown) { return 0; } t->last_fired = now; if (created_ts > t->last_seen_created_at) { t->last_seen_created_at = created_ts; } active_trigger_t trigger_copy = *t; pthread_mutex_unlock(&mgr->mutex); if (trigger_copy.action_type == TRIGGER_ACTION_TEMPLATE) { execute_template_action(mgr, &trigger_copy, event, relay_url); } else { execute_llm_action(&trigger_copy, event, relay_url); } pthread_mutex_lock(&mgr->mutex); 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; } memset(mgr, 0, sizeof(*mgr)); mgr->cfg = cfg; if (pthread_mutex_init(&mgr->mutex, NULL) != 0) { return -1; } int initial_cap = cfg->triggers.max_active > 0 ? cfg->triggers.max_active : TRIGGER_DEFAULT_MAX_ACTIVE; mgr->capacity = initial_cap; mgr->triggers = (active_trigger_t*)calloc((size_t)mgr->capacity, sizeof(active_trigger_t)); if (!mgr->triggers) { pthread_mutex_destroy(&mgr->mutex); memset(mgr, 0, sizeof(*mgr)); return -1; } mgr->last_poll_at = time(NULL); DEBUG_INFO("[didactyl] trigger manager initialized (capacity=%d)", mgr->capacity); return 0; } int trigger_manager_load_from_skills(trigger_manager_t* mgr) { if (!mgr || !mgr->cfg) { return -1; } char* adoption_json = nostr_handler_get_self_events_by_kind_json(10123); if (!adoption_json) { return 0; } cJSON* adoption_events = cJSON_Parse(adoption_json); free(adoption_json); if (!adoption_events || !cJSON_IsArray(adoption_events) || cJSON_GetArraySize(adoption_events) <= 0) { cJSON_Delete(adoption_events); return 0; } cJSON* list_event = cJSON_GetArrayItem(adoption_events, 0); cJSON* list_tags = list_event ? cJSON_GetObjectItemCaseSensitive(list_event, "tags") : NULL; if (!list_tags || !cJSON_IsArray(list_tags)) { cJSON_Delete(adoption_events); return 0; } int loaded = 0; int tn = cJSON_GetArraySize(list_tags); for (int i = 0; i < tn; i++) { cJSON* tag = cJSON_GetArrayItem(list_tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue; cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring || strcmp(key->valuestring, "a") != 0) { continue; } int kind = 0; char pubkey[65] = {0}; char slug[65] = {0}; if (parse_address_tag(val->valuestring, &kind, pubkey, slug) != 0) { continue; } cJSON* skill_events = NULL; if (strcmp(pubkey, mgr->cfg->keys.public_key_hex) == 0) { char* skill_json = nostr_handler_get_self_events_by_kind_json(kind); if (skill_json) { cJSON* all_events = cJSON_Parse(skill_json); free(skill_json); if (all_events && cJSON_IsArray(all_events)) { skill_events = cJSON_CreateArray(); if (skill_events) { int all_n = cJSON_GetArraySize(all_events); for (int ai = 0; ai < all_n; ai++) { cJSON* ev = cJSON_GetArrayItem(all_events, ai); cJSON* ev_pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL; cJSON* ev_tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL; cJSON* ev_d = find_tag_value_string(ev_tags, "d"); if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring || strcmp(ev_pubkey->valuestring, pubkey) != 0 || !ev_d || !cJSON_IsString(ev_d) || !ev_d->valuestring || strcmp(ev_d->valuestring, slug) != 0) { continue; } cJSON* dup = cJSON_Duplicate(ev, 1); if (dup) { cJSON_AddItemToArray(skill_events, dup); } } } } cJSON_Delete(all_events); } } else { cJSON* skill_filter = cJSON_CreateObject(); cJSON* sk_kinds = cJSON_CreateArray(); cJSON* sk_authors = cJSON_CreateArray(); cJSON* d_values = cJSON_CreateArray(); if (!skill_filter || !sk_kinds || !sk_authors || !d_values) { cJSON_Delete(skill_filter); cJSON_Delete(sk_kinds); cJSON_Delete(sk_authors); cJSON_Delete(d_values); continue; } cJSON_AddItemToArray(sk_kinds, cJSON_CreateNumber(kind)); cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds); cJSON_AddItemToArray(sk_authors, cJSON_CreateString(pubkey)); cJSON_AddItemToObject(skill_filter, "authors", sk_authors); cJSON_AddItemToArray(d_values, cJSON_CreateString(slug)); cJSON_AddItemToObject(skill_filter, "#d", d_values); cJSON_AddNumberToObject(skill_filter, "limit", 1); char* skill_json = nostr_handler_query_json(skill_filter, 2000); cJSON_Delete(skill_filter); if (skill_json) { skill_events = cJSON_Parse(skill_json); free(skill_json); } } if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) { cJSON_Delete(skill_events); continue; } cJSON* skill_event = cJSON_GetArrayItem(skill_events, 0); cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL; cJSON* tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL; if (!content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) { cJSON_Delete(skill_events); continue; } cJSON* trigger = find_tag_value_string(tags, "trigger"); cJSON* filter = find_tag_value_string(tags, "filter"); cJSON* action = find_tag_value_string(tags, "action"); cJSON* enabled = find_tag_value_string(tags, "enabled"); const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL; const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL; const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm"; const char* enabled_s = (enabled && cJSON_IsString(enabled) && enabled->valuestring) ? enabled->valuestring : "true"; if (trigger_s && strcmp(trigger_s, "nostr-subscription") == 0 && filter_s && filter_s[0] != '\0') { trigger_action_type_t at = (strcmp(action_s, "template") == 0) ? TRIGGER_ACTION_TEMPLATE : TRIGGER_ACTION_LLM; int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1; if (trigger_manager_add(mgr, slug, content->valuestring, filter_s, at, is_enabled) == 0) { loaded++; } } cJSON_Delete(skill_events); } cJSON_Delete(adoption_events); DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from skills", loaded); return 0; } int trigger_manager_load_from_startup_events(trigger_manager_t* mgr) { if (!mgr || !mgr->cfg) { return -1; } int loaded = 0; int considered = 0; for (int i = 0; i < mgr->cfg->startup_event_count; i++) { startup_event_t* ev = &mgr->cfg->startup_events[i]; if (!ev) { continue; } if (ev->kind != 31123 && ev->kind != 31124) { continue; } considered++; if (!ev->content || ev->content[0] == '\0' || !ev->tags_json || ev->tags_json[0] == '\0') { continue; } cJSON* tags = cJSON_Parse(ev->tags_json); if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(tags); DEBUG_WARN("[didactyl] startup trigger skip: invalid tags_json for startup event index=%d", i); continue; } cJSON* d = find_tag_value_string(tags, "d"); cJSON* trigger = find_tag_value_string(tags, "trigger"); cJSON* filter = find_tag_value_string(tags, "filter"); cJSON* action = find_tag_value_string(tags, "action"); cJSON* enabled = find_tag_value_string(tags, "enabled"); const char* slug = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL; const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL; const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL; const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm"; const char* enabled_s = (enabled && cJSON_IsString(enabled) && enabled->valuestring) ? enabled->valuestring : "true"; if (!slug || slug[0] == '\0') { cJSON_Delete(tags); DEBUG_WARN("[didactyl] startup trigger skip: missing d tag for startup event index=%d", i); continue; } if (!trigger_s || strcmp(trigger_s, "nostr-subscription") != 0 || !filter_s || filter_s[0] == '\0') { cJSON_Delete(tags); continue; } trigger_action_type_t at = (strcmp(action_s, "template") == 0) ? TRIGGER_ACTION_TEMPLATE : TRIGGER_ACTION_LLM; int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1; if (trigger_manager_add(mgr, slug, ev->content, filter_s, at, is_enabled) == 0) { loaded++; DEBUG_INFO("[didactyl] startup trigger registered slug=%s action=%s enabled=%d", slug, at == TRIGGER_ACTION_TEMPLATE ? "template" : "llm", is_enabled); } else { DEBUG_WARN("[didactyl] startup trigger failed slug=%s", slug); } cJSON_Delete(tags); } DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from startup config (considered=%d)", loaded, considered); return 0; } int trigger_manager_add(trigger_manager_t* mgr, const char* skill_slug, const char* content, const char* filter_json, trigger_action_type_t action_type, int enabled) { if (!mgr || !skill_slug || skill_slug[0] == '\0' || !content || !filter_json) { return -1; } if (action_type != TRIGGER_ACTION_LLM && action_type != TRIGGER_ACTION_TEMPLATE) { return -1; } pthread_mutex_lock(&mgr->mutex); int existing = find_trigger_index_locked(mgr, skill_slug); if (existing >= 0) { pthread_mutex_unlock(&mgr->mutex); return trigger_manager_update(mgr, skill_slug, content, filter_json, action_type, enabled); } int max_active = mgr->cfg ? mgr->cfg->triggers.max_active : TRIGGER_DEFAULT_MAX_ACTIVE; if (max_active < 1) max_active = TRIGGER_DEFAULT_MAX_ACTIVE; if (mgr->count >= max_active) { pthread_mutex_unlock(&mgr->mutex); DEBUG_WARN("[didactyl] trigger add rejected: max_active reached (%d)", max_active); return -1; } if (ensure_capacity(mgr, mgr->count + 1) != 0) { pthread_mutex_unlock(&mgr->mutex); return -1; } active_trigger_t* t = &mgr->triggers[mgr->count]; memset(t, 0, sizeof(*t)); snprintf(t->skill_slug, sizeof(t->skill_slug), "%s", skill_slug); snprintf(t->skill_content, sizeof(t->skill_content), "%s", content); snprintf(t->filter_json, sizeof(t->filter_json), "%s", filter_json); t->action_type = action_type; 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++; pthread_mutex_unlock(&mgr->mutex); DEBUG_INFO("[didactyl] trigger added slug=%s action=%d enabled=%d", skill_slug, (int)action_type, clamp_enabled(enabled)); return 0; } int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_slug) { if (!mgr || !skill_slug || skill_slug[0] == '\0') { return -1; } pthread_mutex_lock(&mgr->mutex); int idx = find_trigger_index_locked(mgr, skill_slug); if (idx < 0) { pthread_mutex_unlock(&mgr->mutex); return 0; } close_trigger_subscription_locked(&mgr->triggers[idx]); if (idx < mgr->count - 1) { memmove(&mgr->triggers[idx], &mgr->triggers[idx + 1], (size_t)(mgr->count - idx - 1) * sizeof(active_trigger_t)); } mgr->count--; memset(&mgr->triggers[mgr->count], 0, sizeof(active_trigger_t)); pthread_mutex_unlock(&mgr->mutex); DEBUG_INFO("[didactyl] trigger removed slug=%s", skill_slug); return 0; } int trigger_manager_update(trigger_manager_t* mgr, const char* skill_slug, const char* content, const char* filter_json, trigger_action_type_t action_type, int enabled) { if (!mgr || !skill_slug || skill_slug[0] == '\0' || !content || !filter_json) { return -1; } if (action_type != TRIGGER_ACTION_LLM && action_type != TRIGGER_ACTION_TEMPLATE) { return -1; } pthread_mutex_lock(&mgr->mutex); int idx = find_trigger_index_locked(mgr, skill_slug); if (idx < 0) { pthread_mutex_unlock(&mgr->mutex); return trigger_manager_add(mgr, skill_slug, content, filter_json, action_type, enabled); } active_trigger_t* t = &mgr->triggers[idx]; snprintf(t->skill_content, sizeof(t->skill_content), "%s", content); snprintf(t->filter_json, sizeof(t->filter_json), "%s", filter_json); 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)); return 0; } int trigger_manager_active_count(trigger_manager_t* mgr) { if (!mgr) { return 0; } int active = 0; pthread_mutex_lock(&mgr->mutex); for (int i = 0; i < mgr->count; i++) { if (mgr->triggers[i].enabled) { active++; } } pthread_mutex_unlock(&mgr->mutex); return active; } int trigger_manager_poll(trigger_manager_t* mgr) { (void)mgr; return 0; } char* trigger_manager_status_json(trigger_manager_t* mgr) { if (!mgr) { cJSON* err = cJSON_CreateObject(); if (!err) { return NULL; } cJSON_AddBoolToObject(err, "success", 0); cJSON_AddStringToObject(err, "error", "trigger manager unavailable"); char* out = cJSON_PrintUnformatted(err); cJSON_Delete(err); return out; } cJSON* root = cJSON_CreateObject(); cJSON* arr = cJSON_CreateArray(); if (!root || !arr) { cJSON_Delete(root); cJSON_Delete(arr); return NULL; } pthread_mutex_lock(&mgr->mutex); cJSON_AddBoolToObject(root, "success", 1); cJSON_AddNumberToObject(root, "count", mgr->count); int active = 0; for (int i = 0; i < mgr->count; i++) { active_trigger_t* t = &mgr->triggers[i]; if (t->enabled) { active++; } cJSON* item = cJSON_CreateObject(); if (!item) { continue; } cJSON_AddStringToObject(item, "skill_slug", t->skill_slug); 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); } cJSON_AddNumberToObject(root, "active", active); cJSON_AddItemToObject(root, "triggers", arr); pthread_mutex_unlock(&mgr->mutex); char* out = cJSON_PrintUnformatted(root); cJSON_Delete(root); return out; } void trigger_manager_cleanup(trigger_manager_t* mgr) { if (!mgr) { return; } 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; mgr->capacity = 0; mgr->cfg = NULL; mgr->last_poll_at = 0; pthread_mutex_unlock(&mgr->mutex); pthread_mutex_destroy(&mgr->mutex); memset(mgr, 0, sizeof(*mgr)); DEBUG_INFO("[didactyl] trigger manager cleaned up"); }