Files
didactyl/src/trigger_manager.c
T

1730 lines
56 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "trigger_manager.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include "cjson/cJSON.h"
#include "debug.h"
#include "nostr_handler.h"
#include "llm.h"
void agent_on_trigger(const char* skill_d_tag,
const char* skill_content,
cJSON* triggering_event,
const char* relay_url);
typedef struct {
trigger_manager_t* mgr;
char skill_d_tag[TRIGGER_SKILL_D_TAG_MAX];
} trigger_subscription_ctx_t;
static int clamp_enabled(int enabled) {
return enabled ? 1 : 0;
}
trigger_type_t trigger_type_from_string(const char* s) {
if (!s) {
return TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
}
if (strcmp(s, "webhook") == 0) {
return TRIGGER_TYPE_WEBHOOK;
}
if (strcmp(s, "cron") == 0) {
return TRIGGER_TYPE_CRON;
}
if (strcmp(s, "chain") == 0) {
return TRIGGER_TYPE_CHAIN;
}
if (strcmp(s, "dm") == 0) {
return TRIGGER_TYPE_DM;
}
return TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
}
const char* trigger_type_to_string(trigger_type_t t) {
switch (t) {
case TRIGGER_TYPE_WEBHOOK:
return "webhook";
case TRIGGER_TYPE_CRON:
return "cron";
case TRIGGER_TYPE_CHAIN:
return "chain";
case TRIGGER_TYPE_DM:
return "dm";
case TRIGGER_TYPE_NOSTR_SUBSCRIPTION:
default:
return "nostr-subscription";
}
}
static int is_int_in_csv_list(const char* text, int value) {
if (!text || text[0] == '\0') {
return 0;
}
const char* p = text;
while (*p) {
while (*p == ' ') p++;
char* endptr = NULL;
long n = strtol(p, &endptr, 10);
if (endptr == p) {
return 0;
}
if ((int)n == value) {
return 1;
}
p = endptr;
while (*p == ' ') p++;
if (*p == ',') {
p++;
continue;
}
if (*p == '\0') {
break;
}
return 0;
}
return 0;
}
static int cron_field_token_matches(const char* token, int value, int min_v, int max_v) {
if (!token || token[0] == '\0') {
return 0;
}
if (strcmp(token, "*") == 0) {
return 1;
}
const char* slash = strchr(token, '/');
int step = 0;
char base[64];
if (slash) {
size_t base_len = (size_t)(slash - token);
if (base_len == 0 || base_len >= sizeof(base)) {
return 0;
}
memcpy(base, token, base_len);
base[base_len] = '\0';
char* endptr = NULL;
long step_l = strtol(slash + 1, &endptr, 10);
if (endptr == slash + 1 || *endptr != '\0' || step_l <= 0 || step_l > 1024) {
return 0;
}
step = (int)step_l;
} else {
snprintf(base, sizeof(base), "%s", token);
}
int range_start = min_v;
int range_end = max_v;
if (strcmp(base, "*") == 0) {
range_start = min_v;
range_end = max_v;
} else {
const char* dash = strchr(base, '-');
if (dash) {
char left[32];
char right[32];
size_t left_len = (size_t)(dash - base);
size_t right_len = strlen(dash + 1);
if (left_len == 0 || right_len == 0 || left_len >= sizeof(left) || right_len >= sizeof(right)) {
return 0;
}
memcpy(left, base, left_len);
left[left_len] = '\0';
memcpy(right, dash + 1, right_len + 1U);
char* e1 = NULL;
char* e2 = NULL;
long a = strtol(left, &e1, 10);
long b = strtol(right, &e2, 10);
if (!e1 || *e1 != '\0' || !e2 || *e2 != '\0') {
return 0;
}
range_start = (int)a;
range_end = (int)b;
} else if (strchr(base, ',')) {
if (!is_int_in_csv_list(base, value)) {
return 0;
}
if (step <= 1) {
return 1;
}
return ((value - min_v) % step) == 0;
} else {
char* e = NULL;
long n = strtol(base, &e, 10);
if (!e || *e != '\0') {
return 0;
}
range_start = (int)n;
range_end = (int)n;
}
}
if (range_start < min_v || range_end > max_v || range_start > range_end) {
return 0;
}
if (value < range_start || value > range_end) {
return 0;
}
if (step > 1 && ((value - range_start) % step) != 0) {
return 0;
}
return 1;
}
static int cron_field_matches(const char* field, int value, int min_v, int max_v) {
if (!field || field[0] == '\0') {
return 0;
}
char tmp[128];
snprintf(tmp, sizeof(tmp), "%s", field);
char* saveptr = NULL;
char* token = strtok_r(tmp, ",", &saveptr);
while (token) {
while (*token == ' ') token++;
if (cron_field_token_matches(token, value, min_v, max_v)) {
return 1;
}
token = strtok_r(NULL, ",", &saveptr);
}
return 0;
}
static int cron_normalize_expression(const char* expr, char* out, size_t out_sz, int* out_expanded) {
if (out_expanded) {
*out_expanded = 0;
}
if (!expr || !out || out_sz == 0) {
return -1;
}
while (*expr && isspace((unsigned char)*expr)) {
expr++;
}
if (*expr == '\0') {
return -1;
}
const char* mapped = NULL;
if (strcmp(expr, "*") == 0) {
mapped = "* * * * *";
} else if (strcmp(expr, "@yearly") == 0 || strcmp(expr, "@annually") == 0) {
mapped = "0 0 1 1 *";
} else if (strcmp(expr, "@monthly") == 0) {
mapped = "0 0 1 * *";
} else if (strcmp(expr, "@weekly") == 0) {
mapped = "0 0 * * 0";
} else if (strcmp(expr, "@daily") == 0 || strcmp(expr, "@midnight") == 0) {
mapped = "0 0 * * *";
} else if (strcmp(expr, "@hourly") == 0) {
mapped = "0 * * * *";
}
const char* src = mapped ? mapped : expr;
int n = snprintf(out, out_sz, "%s", src);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
if (mapped && out_expanded) {
*out_expanded = 1;
}
return 0;
}
static int cron_field_token_valid(const char* token, int min_v, int max_v) {
if (!token || token[0] == '\0') {
return 0;
}
if (strcmp(token, "*") == 0) {
return 1;
}
const char* slash = strchr(token, '/');
int step = 0;
char base[64];
if (slash) {
size_t base_len = (size_t)(slash - token);
if (base_len == 0 || base_len >= sizeof(base)) {
return 0;
}
memcpy(base, token, base_len);
base[base_len] = '\0';
char* endptr = NULL;
long step_l = strtol(slash + 1, &endptr, 10);
if (endptr == slash + 1 || *endptr != '\0' || step_l <= 0 || step_l > 1024) {
return 0;
}
step = (int)step_l;
} else {
int n = snprintf(base, sizeof(base), "%s", token);
if (n < 0 || (size_t)n >= sizeof(base)) {
return 0;
}
}
if (strcmp(base, "*") == 0) {
return step == 0 || step <= (max_v - min_v + 1);
}
const char* dash = strchr(base, '-');
if (dash) {
char left[32];
char right[32];
size_t left_len = (size_t)(dash - base);
size_t right_len = strlen(dash + 1);
if (left_len == 0 || right_len == 0 || left_len >= sizeof(left) || right_len >= sizeof(right)) {
return 0;
}
memcpy(left, base, left_len);
left[left_len] = '\0';
memcpy(right, dash + 1, right_len + 1U);
char* e1 = NULL;
char* e2 = NULL;
long a = strtol(left, &e1, 10);
long b = strtol(right, &e2, 10);
if (!e1 || *e1 != '\0' || !e2 || *e2 != '\0') {
return 0;
}
if ((int)a < min_v || (int)b > max_v || (int)a > (int)b) {
return 0;
}
return 1;
}
char* e = NULL;
long n = strtol(base, &e, 10);
if (!e || *e != '\0') {
return 0;
}
if ((int)n < min_v || (int)n > max_v) {
return 0;
}
return 1;
}
static int cron_field_valid(const char* field, int min_v, int max_v) {
if (!field || field[0] == '\0') {
return 0;
}
char tmp[128];
int n = snprintf(tmp, sizeof(tmp), "%s", field);
if (n < 0 || (size_t)n >= sizeof(tmp)) {
return 0;
}
int tokens = 0;
char* saveptr = NULL;
char* token = strtok_r(tmp, ",", &saveptr);
while (token) {
while (*token == ' ') token++;
if (!cron_field_token_valid(token, min_v, max_v)) {
return 0;
}
tokens++;
token = strtok_r(NULL, ",", &saveptr);
}
return tokens > 0;
}
static int cron_expr_is_valid(const char* expr, char* normalized_out, size_t normalized_out_size) {
if (!normalized_out || normalized_out_size == 0) {
return 0;
}
if (cron_normalize_expression(expr, normalized_out, normalized_out_size, NULL) != 0) {
return 0;
}
char parse_buf[TRIGGER_FILTER_JSON_MAX];
int n = snprintf(parse_buf, sizeof(parse_buf), "%s", normalized_out);
if (n < 0 || (size_t)n >= sizeof(parse_buf)) {
return 0;
}
char* fields[5] = {0};
int nf = 0;
char* saveptr = NULL;
char* tok = strtok_r(parse_buf, " \t", &saveptr);
while (tok && nf < 5) {
fields[nf++] = tok;
tok = strtok_r(NULL, " \t", &saveptr);
}
if (nf != 5 || tok != NULL) {
return 0;
}
if (!cron_field_valid(fields[0], 0, 59)) return 0;
if (!cron_field_valid(fields[1], 0, 23)) return 0;
if (!cron_field_valid(fields[2], 1, 31)) return 0;
if (!cron_field_valid(fields[3], 1, 12)) return 0;
if (!cron_field_valid(fields[4], 0, 6)) return 0;
return 1;
}
static int cron_matches_now(const char* expr, time_t now_ts) {
if (!expr || expr[0] == '\0') {
return 0;
}
char normalized[TRIGGER_FILTER_JSON_MAX];
if (!cron_expr_is_valid(expr, normalized, sizeof(normalized))) {
return 0;
}
char parse_buf[TRIGGER_FILTER_JSON_MAX];
snprintf(parse_buf, sizeof(parse_buf), "%s", normalized);
char* fields[5] = {0};
int nf = 0;
char* saveptr = NULL;
char* tok = strtok_r(parse_buf, " \t", &saveptr);
while (tok && nf < 5) {
fields[nf++] = tok;
tok = strtok_r(NULL, " \t", &saveptr);
}
if (nf != 5 || tok != NULL) {
return 0;
}
struct tm tm_now;
localtime_r(&now_ts, &tm_now);
int minute = tm_now.tm_min;
int hour = tm_now.tm_hour;
int mday = tm_now.tm_mday;
int month = tm_now.tm_mon + 1;
int wday = tm_now.tm_wday;
if (!cron_field_matches(fields[0], minute, 0, 59)) return 0;
if (!cron_field_matches(fields[1], hour, 0, 23)) return 0;
if (!cron_field_matches(fields[2], mday, 1, 31)) return 0;
if (!cron_field_matches(fields[3], month, 1, 12)) return 0;
if (!cron_field_matches(fields[4], wday, 0, 6)) return 0;
return 1;
}
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_d_tag) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0') {
return -1;
}
for (int i = 0; i < mgr->count; i++) {
if (strncmp(mgr->triggers[i].skill_d_tag, skill_d_tag, TRIGGER_SKILL_D_TAG_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 __attribute__((unused)) int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_d_tag[65]) {
if (!addr || !out_kind || !out_pubkey || !out_d_tag) {
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 d_tag_len = strlen(p2 + 1);
if (d_tag_len == 0 || d_tag_len >= 65U) return -1;
memcpy(out_d_tag, p2 + 1, d_tag_len + 1U);
*out_kind = kind;
return 0;
}
static const char* trim_ws_local(const char* s) {
while (s && *s && isspace((unsigned char)*s)) s++;
return s ? s : "";
}
static void parse_trigger_runtime_tags(cJSON* tags,
const char** out_llm,
const char** out_tools,
int* out_has_max_tokens,
int* out_max_tokens,
int* out_has_temperature,
double* out_temperature,
int* out_has_seed,
int* out_seed) {
if (out_llm) *out_llm = NULL;
if (out_tools) *out_tools = NULL;
if (out_has_max_tokens) *out_has_max_tokens = 0;
if (out_max_tokens) *out_max_tokens = 0;
if (out_has_temperature) *out_has_temperature = 0;
if (out_temperature) *out_temperature = 0.0;
if (out_has_seed) *out_has_seed = 0;
if (out_seed) *out_seed = 0;
if (!tags || !cJSON_IsArray(tags)) {
return;
}
cJSON* llm = find_tag_value_string(tags, "llm");
cJSON* tools = find_tag_value_string(tags, "tools");
cJSON* max_tokens = find_tag_value_string(tags, "max_tokens");
cJSON* temperature = find_tag_value_string(tags, "temperature");
cJSON* seed = find_tag_value_string(tags, "seed");
if (out_llm && llm && cJSON_IsString(llm) && llm->valuestring && llm->valuestring[0] != '\0') {
*out_llm = llm->valuestring;
}
if (out_tools && tools && cJSON_IsString(tools) && tools->valuestring && tools->valuestring[0] != '\0') {
*out_tools = tools->valuestring;
}
if (out_has_max_tokens && out_max_tokens && max_tokens && cJSON_IsString(max_tokens) && max_tokens->valuestring) {
char* endptr = NULL;
long n = strtol(trim_ws_local(max_tokens->valuestring), &endptr, 10);
if (endptr && *trim_ws_local(endptr) == '\0' && n > 0 && n <= 1000000) {
*out_has_max_tokens = 1;
*out_max_tokens = (int)n;
}
}
if (out_has_temperature && out_temperature && temperature && cJSON_IsString(temperature) && temperature->valuestring) {
char* endptr = NULL;
double n = strtod(trim_ws_local(temperature->valuestring), &endptr);
if (endptr && *trim_ws_local(endptr) == '\0' && n >= 0.0 && n <= 2.0) {
*out_has_temperature = 1;
*out_temperature = n;
}
}
if (out_has_seed && out_seed && seed && cJSON_IsString(seed) && seed->valuestring) {
char* endptr = NULL;
long n = strtol(trim_ws_local(seed->valuestring), &endptr, 10);
if (endptr && *trim_ws_local(endptr) == '\0') {
*out_has_seed = 1;
*out_seed = (int)n;
}
}
}
static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
if (!mgr || !mgr->cfg) {
return -1;
}
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!filter || !kinds || !authors) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123));
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(mgr->cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 300);
char* events_json = nostr_handler_query_json(filter, 8000);
cJSON_Delete(filter);
if (!events_json) {
DEBUG_WARN("[didactyl] trigger reconcile skipped: relay query returned no data");
return -1;
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
DEBUG_WARN("[didactyl] trigger reconcile skipped: invalid JSON from relay query");
return -1;
}
int considered = 0;
int loaded = 0;
int event_count = cJSON_GetArraySize(events);
for (int i = 0; i < event_count; i++) {
cJSON* skill_event = cJSON_GetArrayItem(events, i);
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)) {
continue;
}
cJSON* d = find_tag_value_string(tags, "d");
cJSON* trigger = find_tag_value_string(tags, "trigger");
cJSON* filter_j = find_tag_value_string(tags, "filter");
cJSON* action = find_tag_value_string(tags, "action");
const char* d_tag = (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_j && cJSON_IsString(filter_j) && filter_j->valuestring) ? filter_j->valuestring : NULL;
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
considered++;
if (!d_tag || d_tag[0] == '\0') {
continue;
}
int trigger_supported = trigger_s &&
(strcmp(trigger_s, "nostr-subscription") == 0 ||
strcmp(trigger_s, "webhook") == 0 ||
strcmp(trigger_s, "cron") == 0 ||
strcmp(trigger_s, "chain") == 0 ||
strcmp(trigger_s, "dm") == 0);
if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
continue;
}
if (strcmp(action_s, "template") == 0) {
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
}
const char* llm_s = NULL;
const char* tools_s = NULL;
int has_max_tokens = 0;
int max_tokens = 0;
int has_temperature = 0;
double temperature = 0.0;
int has_seed = 0;
int seed = 0;
parse_trigger_runtime_tags(tags,
&llm_s,
&tools_s,
&has_max_tokens,
&max_tokens,
&has_temperature,
&temperature,
&has_seed,
&seed);
if (trigger_manager_add(mgr,
d_tag,
content->valuestring,
filter_s,
TRIGGER_ACTION_LLM,
trigger_s,
1,
llm_s,
tools_s,
has_max_tokens,
max_tokens,
has_temperature,
temperature,
has_seed,
seed) == 0) {
loaded++;
}
}
cJSON_Delete(events);
DEBUG_INFO("[didactyl] trigger reconcile complete: loaded=%d considered=%d", loaded, considered);
return 0;
}
static void apply_trigger_runtime_to_llm_config(const active_trigger_t* t, llm_config_t* cfg) {
if (!t || !cfg) return;
if (t->llm_spec[0] != '\0') {
const char* spec = t->llm_spec;
while (*spec && isspace((unsigned char)*spec)) spec++;
const char* comma = strchr(spec, ',');
size_t len = comma ? (size_t)(comma - spec) : strlen(spec);
while (len > 0 && isspace((unsigned char)spec[len - 1])) len--;
const char* slash = memchr(spec, '/', len);
if (slash) {
size_t provider_len = (size_t)(slash - spec);
const char* model = slash + 1;
size_t model_len = len - (size_t)(model - spec);
while (provider_len > 0 && isspace((unsigned char)spec[provider_len - 1])) provider_len--;
while (model_len > 0 && isspace((unsigned char)*model)) {
model++;
model_len--;
}
while (model_len > 0 && isspace((unsigned char)model[model_len - 1])) model_len--;
if (provider_len > 0 && provider_len < sizeof(cfg->provider)) {
snprintf(cfg->provider, sizeof(cfg->provider), "%.*s", (int)provider_len, spec);
}
if (model_len > 0 && model_len < sizeof(cfg->model)) {
snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)model_len, model);
}
} else if (len > 0 && len < sizeof(cfg->model)) {
snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)len, spec);
}
}
if (t->has_max_tokens && t->max_tokens > 0) {
cfg->max_tokens = t->max_tokens;
}
if (t->has_temperature) {
cfg->temperature = t->temperature;
}
}
static void execute_llm_action(const active_trigger_t* t, cJSON* event, const char* relay_url) {
if (!t || !event) {
return;
}
llm_config_t old_cfg;
int had_old_cfg = (llm_get_config(&old_cfg) == 0);
int overridden = 0;
if (had_old_cfg &&
(t->llm_spec[0] != '\0' || t->has_max_tokens || t->has_temperature)) {
llm_config_t next = old_cfg;
apply_trigger_runtime_to_llm_config(t, &next);
if (llm_set_config(&next) == 0) {
overridden = 1;
}
}
agent_on_trigger(t->skill_d_tag, t->skill_content, event, relay_url);
if (overridden) {
(void)llm_set_config(&old_cfg);
}
}
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);
(void)mgr;
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;
}
(void)trigger_manager_fire(ctx->mgr, ctx->skill_d_tag, event, relay_url);
}
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->trigger_type != TRIGGER_TYPE_NOSTR_SUBSCRIPTION) {
return 0;
}
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_d_tag, sizeof(ctx->skill_d_tag), "%s", t->skill_d_tag);
nostr_pool_subscription_t* sub = nostr_handler_subscribe_with_filter(
filter,
on_trigger_subscription_event,
on_trigger_subscription_eose,
ctx,
0,
0,
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 = 0;
mgr->last_reconcile_at = 0;
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;
}
int loaded = 0;
int considered = 0;
const int kinds[2] = {31123, 31124};
for (int k = 0; k < 2; k++) {
char* skill_json = nostr_handler_get_self_events_by_kind_json(kinds[k]);
if (!skill_json) {
continue;
}
cJSON* skill_events = cJSON_Parse(skill_json);
free(skill_json);
if (!skill_events || !cJSON_IsArray(skill_events)) {
cJSON_Delete(skill_events);
continue;
}
int sn = cJSON_GetArraySize(skill_events);
for (int i = 0; i < sn; i++) {
cJSON* skill_event = cJSON_GetArrayItem(skill_events, i);
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)) {
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");
const char* d_tag = (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";
considered++;
if (!d_tag || d_tag[0] == '\0') {
continue;
}
int trigger_supported = trigger_s &&
(strcmp(trigger_s, "nostr-subscription") == 0 ||
strcmp(trigger_s, "webhook") == 0 ||
strcmp(trigger_s, "cron") == 0 ||
strcmp(trigger_s, "chain") == 0 ||
strcmp(trigger_s, "dm") == 0);
if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
continue;
}
if (strcmp(action_s, "template") == 0) {
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
}
const char* llm_s = NULL;
const char* tools_s = NULL;
int has_max_tokens = 0;
int max_tokens = 0;
int has_temperature = 0;
double temperature = 0.0;
int has_seed = 0;
int seed = 0;
parse_trigger_runtime_tags(tags,
&llm_s,
&tools_s,
&has_max_tokens,
&max_tokens,
&has_temperature,
&temperature,
&has_seed,
&seed);
if (trigger_manager_add(mgr,
d_tag,
content->valuestring,
filter_s,
TRIGGER_ACTION_LLM,
trigger_s,
1,
llm_s,
tools_s,
has_max_tokens,
max_tokens,
has_temperature,
temperature,
has_seed,
seed) == 0) {
loaded++;
}
}
cJSON_Delete(skill_events);
}
DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from self skills (considered=%d)", loaded, considered);
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* d_tag = (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 (!d_tag || d_tag[0] == '\0') {
cJSON_Delete(tags);
DEBUG_WARN("[didactyl] startup trigger skip: missing d tag for startup event index=%d", i);
continue;
}
int trigger_supported = trigger_s &&
(strcmp(trigger_s, "nostr-subscription") == 0 ||
strcmp(trigger_s, "webhook") == 0 ||
strcmp(trigger_s, "cron") == 0 ||
strcmp(trigger_s, "chain") == 0 ||
strcmp(trigger_s, "dm") == 0);
if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
cJSON_Delete(tags);
continue;
}
if (strcmp(action_s, "template") == 0) {
DEBUG_WARN("[didactyl] startup trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
}
int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
const char* llm_s = NULL;
const char* tools_s = NULL;
int has_max_tokens = 0;
int max_tokens = 0;
int has_temperature = 0;
double temperature = 0.0;
int has_seed = 0;
int seed = 0;
parse_trigger_runtime_tags(tags,
&llm_s,
&tools_s,
&has_max_tokens,
&max_tokens,
&has_temperature,
&temperature,
&has_seed,
&seed);
if (trigger_manager_add(mgr,
d_tag,
ev->content,
filter_s,
TRIGGER_ACTION_LLM,
trigger_s,
is_enabled,
llm_s,
tools_s,
has_max_tokens,
max_tokens,
has_temperature,
temperature,
has_seed,
seed) == 0) {
loaded++;
DEBUG_INFO("[didactyl] startup trigger registered d_tag=%s action=%s enabled=%d", d_tag, "llm", is_enabled);
} else {
DEBUG_WARN("[didactyl] startup trigger failed d_tag=%s", d_tag);
}
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_d_tag,
const char* content,
const char* filter_json,
trigger_action_type_t action_type,
const char* trigger_type_str,
int enabled,
const char* llm_spec,
const char* tools_policy,
int has_max_tokens,
int max_tokens,
int has_temperature,
double temperature,
int has_seed,
int seed) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !content || !filter_json) {
return -1;
}
if (action_type == TRIGGER_ACTION_TEMPLATE) {
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", skill_d_tag);
action_type = TRIGGER_ACTION_LLM;
}
if (action_type != TRIGGER_ACTION_LLM) {
return -1;
}
pthread_mutex_lock(&mgr->mutex);
int existing = find_trigger_index_locked(mgr, skill_d_tag);
if (existing >= 0) {
pthread_mutex_unlock(&mgr->mutex);
return trigger_manager_update(mgr,
skill_d_tag,
content,
filter_json,
action_type,
trigger_type_str,
enabled,
llm_spec,
tools_policy,
has_max_tokens,
max_tokens,
has_temperature,
temperature,
has_seed,
seed);
}
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_d_tag, sizeof(t->skill_d_tag), "%s", skill_d_tag);
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->trigger_type = trigger_type_from_string(trigger_type_str);
t->last_fired = 0;
t->last_seen_created_at = 0;
t->last_cron_fire = 0;
if (t->trigger_type == TRIGGER_TYPE_CRON) {
char normalized_cron[sizeof(t->cron_expr)] = {0};
if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
pthread_mutex_unlock(&mgr->mutex);
DEBUG_WARN("[didactyl] trigger add rejected: invalid cron expression d_tag=%s expr=%s", skill_d_tag, filter_json);
memset(t, 0, sizeof(*t));
return -1;
}
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", normalized_cron);
}
snprintf(t->llm_spec, sizeof(t->llm_spec), "%s", llm_spec ? llm_spec : "");
snprintf(t->tools_policy, sizeof(t->tools_policy), "%s", tools_policy ? tools_policy : "");
t->has_max_tokens = has_max_tokens ? 1 : 0;
t->max_tokens = max_tokens;
t->has_temperature = has_temperature ? 1 : 0;
t->temperature = temperature;
t->has_seed = has_seed ? 1 : 0;
t->seed = seed;
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 d_tag=%s", skill_d_tag);
memset(t, 0, sizeof(*t));
return -1;
}
mgr->count++;
pthread_mutex_unlock(&mgr->mutex);
DEBUG_INFO("[didactyl] trigger added d_tag=%s action=%d enabled=%d", skill_d_tag, (int)action_type, clamp_enabled(enabled));
return 0;
}
int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_d_tag) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0') {
return -1;
}
pthread_mutex_lock(&mgr->mutex);
int idx = find_trigger_index_locked(mgr, skill_d_tag);
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 d_tag=%s", skill_d_tag);
return 0;
}
int trigger_manager_find(trigger_manager_t* mgr, const char* skill_d_tag, active_trigger_t* out) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !out) {
return -1;
}
pthread_mutex_lock(&mgr->mutex);
int idx = find_trigger_index_locked(mgr, skill_d_tag);
if (idx < 0) {
pthread_mutex_unlock(&mgr->mutex);
return -1;
}
*out = mgr->triggers[idx];
out->subscription = NULL;
out->subscription_ctx = NULL;
pthread_mutex_unlock(&mgr->mutex);
return 0;
}
int trigger_manager_fire(trigger_manager_t* mgr,
const char* skill_d_tag,
cJSON* event,
const char* source_label) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !event) {
return -1;
}
pthread_mutex_lock(&mgr->mutex);
int idx = find_trigger_index_locked(mgr, skill_d_tag);
if (idx < 0) {
pthread_mutex_unlock(&mgr->mutex);
return -1;
}
int fired = maybe_fire_trigger_locked(mgr, idx, event, source_label);
pthread_mutex_unlock(&mgr->mutex);
return fired;
}
int trigger_manager_fire_chains(trigger_manager_t* mgr,
const char* source_skill_d_tag,
cJSON* source_event,
const char* source_label) {
if (!mgr || !source_skill_d_tag || source_skill_d_tag[0] == '\0') {
return -1;
}
static __thread int s_chain_depth = 0;
if (s_chain_depth >= 5) {
DEBUG_WARN("[didactyl] chain trigger depth limit reached for source=%s", source_skill_d_tag);
return 0;
}
int fired_count = 0;
s_chain_depth++;
pthread_mutex_lock(&mgr->mutex);
int count_snapshot = mgr->count;
for (int i = 0; i < count_snapshot; i++) {
active_trigger_t* t = &mgr->triggers[i];
if (!t->enabled || t->trigger_type != TRIGGER_TYPE_CHAIN) {
continue;
}
if (t->filter_json[0] == '\0' || strcmp(t->filter_json, source_skill_d_tag) != 0) {
continue;
}
time_t now = time(NULL);
int cooldown = mgr->cfg ? mgr->cfg->triggers.cooldown_seconds : 0;
if (cooldown < 0) cooldown = 0;
if (cooldown > 0 && t->last_fired > 0 && (now - t->last_fired) < cooldown) {
continue;
}
t->last_fired = now;
active_trigger_t trigger_copy = *t;
trigger_copy.subscription = NULL;
trigger_copy.subscription_ctx = NULL;
pthread_mutex_unlock(&mgr->mutex);
cJSON* event = cJSON_CreateObject();
if (event) {
cJSON_AddStringToObject(event, "type", "chain");
cJSON_AddStringToObject(event, "source_d_tag", source_skill_d_tag);
cJSON_AddStringToObject(event, "source_label", source_label ? source_label : "chain");
cJSON_AddNumberToObject(event, "created_at", (double)now);
if (source_event) {
cJSON* dup = cJSON_Duplicate(source_event, 1);
if (dup) {
cJSON_AddItemToObject(event, "source_event", dup);
}
}
(void)mgr;
execute_llm_action(&trigger_copy, event, "chain");
cJSON_Delete(event);
fired_count++;
}
pthread_mutex_lock(&mgr->mutex);
}
pthread_mutex_unlock(&mgr->mutex);
s_chain_depth--;
return fired_count;
}
int trigger_manager_update(trigger_manager_t* mgr,
const char* skill_d_tag,
const char* content,
const char* filter_json,
trigger_action_type_t action_type,
const char* trigger_type_str,
int enabled,
const char* llm_spec,
const char* tools_policy,
int has_max_tokens,
int max_tokens,
int has_temperature,
double temperature,
int has_seed,
int seed) {
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !content || !filter_json) {
return -1;
}
if (action_type == TRIGGER_ACTION_TEMPLATE) {
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", skill_d_tag);
action_type = TRIGGER_ACTION_LLM;
}
if (action_type != TRIGGER_ACTION_LLM) {
return -1;
}
pthread_mutex_lock(&mgr->mutex);
int idx = find_trigger_index_locked(mgr, skill_d_tag);
if (idx < 0) {
pthread_mutex_unlock(&mgr->mutex);
return trigger_manager_add(mgr,
skill_d_tag,
content,
filter_json,
action_type,
trigger_type_str,
enabled,
llm_spec,
tools_policy,
has_max_tokens,
max_tokens,
has_temperature,
temperature,
has_seed,
seed);
}
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);
t->trigger_type = trigger_type_from_string(trigger_type_str);
if (t->trigger_type == TRIGGER_TYPE_CRON) {
char normalized_cron[sizeof(t->cron_expr)] = {0};
if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
pthread_mutex_unlock(&mgr->mutex);
DEBUG_WARN("[didactyl] trigger update rejected: invalid cron expression d_tag=%s expr=%s", skill_d_tag, filter_json);
return -1;
}
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", normalized_cron);
} else {
t->cron_expr[0] = '\0';
}
snprintf(t->llm_spec, sizeof(t->llm_spec), "%s", llm_spec ? llm_spec : "");
snprintf(t->tools_policy, sizeof(t->tools_policy), "%s", tools_policy ? tools_policy : "");
t->has_max_tokens = has_max_tokens ? 1 : 0;
t->max_tokens = max_tokens;
t->has_temperature = has_temperature ? 1 : 0;
t->temperature = temperature;
t->has_seed = has_seed ? 1 : 0;
t->seed = seed;
if (register_trigger_subscription_locked(mgr, t) != 0) {
pthread_mutex_unlock(&mgr->mutex);
DEBUG_WARN("[didactyl] trigger update failed to (re)subscribe d_tag=%s", skill_d_tag);
return -1;
}
pthread_mutex_unlock(&mgr->mutex);
DEBUG_INFO("[didactyl] trigger updated d_tag=%s action=%d enabled=%d", skill_d_tag, (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) {
if (!mgr) {
return -1;
}
time_t now = time(NULL);
if (now <= 0) {
return 0;
}
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 30) {
return 0;
}
mgr->last_poll_at = now;
int fired = 0;
pthread_mutex_lock(&mgr->mutex);
int count_snapshot = mgr->count;
for (int i = 0; i < count_snapshot; i++) {
active_trigger_t* t = &mgr->triggers[i];
if (!t->enabled || t->trigger_type != TRIGGER_TYPE_CRON) {
continue;
}
const char* expr = (t->cron_expr[0] != '\0') ? t->cron_expr : t->filter_json;
char normalized_expr[64] = {0};
if (!cron_expr_is_valid(expr, normalized_expr, sizeof(normalized_expr))) {
DEBUG_WARN("[didactyl] cron trigger skipped: invalid expression d_tag=%s expr=%s", t->skill_d_tag, expr ? expr : "");
continue;
}
if (!cron_matches_now(normalized_expr, now)) {
continue;
}
DEBUG_INFO("[didactyl] cron trigger matched d_tag=%s expr=%s", t->skill_d_tag, normalized_expr);
if (t->last_cron_fire > 0 && (now - t->last_cron_fire) < 50) {
continue;
}
t->last_cron_fire = now;
active_trigger_t trigger_copy = *t;
trigger_copy.subscription = NULL;
trigger_copy.subscription_ctx = NULL;
pthread_mutex_unlock(&mgr->mutex);
cJSON* event = cJSON_CreateObject();
if (event) {
cJSON_AddStringToObject(event, "type", "cron");
cJSON_AddStringToObject(event, "d_tag", trigger_copy.skill_d_tag);
cJSON_AddStringToObject(event, "cron_expr", normalized_expr);
cJSON_AddNumberToObject(event, "created_at", (double)now);
(void)mgr;
execute_llm_action(&trigger_copy, event, "cron");
cJSON_Delete(event);
fired++;
}
pthread_mutex_lock(&mgr->mutex);
}
pthread_mutex_unlock(&mgr->mutex);
if (mgr->last_reconcile_at <= 0 || (now - mgr->last_reconcile_at) >= 900) {
mgr->last_reconcile_at = now;
(void)trigger_manager_reconcile_from_relays(mgr);
}
return fired;
}
static int dm_filter_matches_tier(const char* filter_json, didactyl_sender_tier_t tier) {
if (!filter_json || filter_json[0] == '\0') {
return 0;
}
cJSON* root = cJSON_Parse(filter_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return 0;
}
cJSON* from = cJSON_GetObjectItemCaseSensitive(root, "from");
const char* from_s = (from && cJSON_IsString(from) && from->valuestring) ? from->valuestring : "admin";
int match = 0;
if (strcmp(from_s, "any") == 0) {
match = 1;
} else if (strcmp(from_s, "admin") == 0) {
match = (tier == DIDACTYL_SENDER_ADMIN);
} else if (strcmp(from_s, "wot") == 0) {
match = (tier == DIDACTYL_SENDER_WOT);
}
cJSON_Delete(root);
return match;
}
int trigger_manager_get_dm_skills(trigger_manager_t* mgr,
const char* sender_pubkey_hex,
didactyl_sender_tier_t tier,
char** out_d_tags,
int max_d_tags) {
(void)sender_pubkey_hex;
if (!mgr || !out_d_tags || max_d_tags <= 0) {
return -1;
}
int count = 0;
pthread_mutex_lock(&mgr->mutex);
int count_snapshot = mgr->count;
for (int i = 0; i < count_snapshot && count < max_d_tags; i++) {
active_trigger_t* t = &mgr->triggers[i];
if (!t->enabled || t->trigger_type != TRIGGER_TYPE_DM) {
continue;
}
if (!dm_filter_matches_tier(t->filter_json, tier)) {
continue;
}
out_d_tags[count++] = t->skill_d_tag;
}
pthread_mutex_unlock(&mgr->mutex);
return count;
}
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_d_tag", t->skill_d_tag);
cJSON_AddStringToObject(item, "filter_json", t->filter_json);
cJSON_AddStringToObject(item, "type", trigger_type_to_string(t->trigger_type));
cJSON_AddStringToObject(item, "action", "llm");
cJSON_AddBoolToObject(item, "enabled", t->enabled ? 1 : 0);
cJSON_AddBoolToObject(item, "subscribed", t->subscription ? 1 : 0);
cJSON_AddStringToObject(item, "llm", t->llm_spec);
cJSON_AddStringToObject(item, "tools", t->tools_policy);
cJSON_AddBoolToObject(item, "has_max_tokens", t->has_max_tokens ? 1 : 0);
if (t->has_max_tokens) cJSON_AddNumberToObject(item, "max_tokens", t->max_tokens);
cJSON_AddBoolToObject(item, "has_temperature", t->has_temperature ? 1 : 0);
if (t->has_temperature) cJSON_AddNumberToObject(item, "temperature", t->temperature);
cJSON_AddBoolToObject(item, "has_seed", t->has_seed ? 1 : 0);
if (t->has_seed) cJSON_AddNumberToObject(item, "seed", t->seed);
cJSON_AddNumberToObject(item, "last_fired", (double)t->last_fired);
cJSON_AddNumberToObject(item, "last_seen_created_at", (double)t->last_seen_created_at);
if (t->trigger_type == TRIGGER_TYPE_CRON) {
cJSON_AddStringToObject(item, "cron_expr", t->cron_expr);
cJSON_AddNumberToObject(item, "last_cron_fire", (double)t->last_cron_fire);
}
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");
}