Compare commits

...
11 Commits
Author SHA1 Message Date
Didactyl User d13044d267 chore(agent): remove dead DM-history helper functions and orphaned constants 2026-04-28 07:34:17 -04:00
Didactyl User 8bb90a31a5 v0.2.48 - Remove admin kind 10123 from subscription and upsert, add debug logging 2026-04-22 10:58:07 -04:00
Didactyl User b6ca2bf3bf v0.2.47 - nostr: exclude admin from kind 10123 self-skill adoption and add self-skill upsert debug logs 2026-04-21 06:35:53 -04:00
Didactyl User 73c368e4e5 v0.2.46 - Finalize skill_list formatting cleanup and adopted-author cache refresh improvements 2026-04-16 18:15:55 -04:00
Didactyl User 2a091aabf7 v0.2.45 - Expand skill refresh to include adopted authors and clean up skill_list output 2026-04-16 18:06:20 -04:00
Didactyl User c877ca646e v0.2.44 - Fix adoption list selection to self-authored latest event in agent and skill tool 2026-04-16 17:25:47 -04:00
Didactyl User b5b479e9a8 v0.2.43 - Fix /help adopted-skills section to use latest kind 10123 event by created_at in CLI and HTTP views 2026-04-16 16:59:50 -04:00
Didactyl User 52f1cd3b30 v0.2.42 - Make skill_refresh re-query relays and reload self-skill cache before invalidating adopted-skills cache 2026-04-16 16:38:11 -04:00
Didactyl User 09d45febdd v0.2.41 - Fix adopted-skills cache to select latest kind 10123 event by created_at in agent refresh path 2026-04-15 14:12:44 -04:00
Didactyl User b9e90f0b21 v0.2.40 - Fix adoption list freshness by selecting newest kind 10123 event and temporarily disable periodic relay-backed trigger reconciliation for validation 2026-04-14 17:47:57 -04:00
Didactyl User 59bd2603e2 v0.2.39 - Add 15-minute relay-backed trigger reconciliation in trigger_manager_poll to recover missed self-skill events and re-register missing triggers 2026-04-14 16:36:53 -04:00
10 changed files with 837 additions and 140 deletions
+2 -2
View File
@@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
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.2.38
## Current Status — v0.2.48
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.2.38 — Resolve NIP-17 gift wrap send failure investigation and docs
> Last release update: v0.2.48 — Remove admin kind 10123 from subscription and upsert, add debug logging
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+247
View File
@@ -0,0 +1,247 @@
# Cron Trigger Pipeline: How It Works and How to Make It Robust
## Current Architecture: Skill Events → Trigger Registration
The pipeline has **two paths** for getting a cron trigger loaded, plus a **polling loop** that actually fires them.
### Path 1: Bulk load at startup (EOSE-driven)
```mermaid
sequenceDiagram
participant Main as main.c startup
participant NH as nostr_handler
participant Relays as Nostr Relays
participant TM as trigger_manager
Main->>TM: trigger_manager_init - capacity=16, last_poll_at=0
Main->>TM: trigger_manager_load_from_startup_events
Note over TM: Loads triggers from genesis startup_events<br/>only dm triggers typically here
Main->>NH: nostr_handler_set_self_skill_eose_callback<br/>registers on_self_skill_eose_load_triggers
Main->>NH: nostr_handler_subscribe_self_skills
NH->>Relays: REQ kinds 31123, 31124, 10123<br/>authors = agent pubkey
Relays-->>NH: EVENT skill events arrive one by one
NH->>NH: on_self_skill_event for each<br/>caches in g_self_skill_events<br/>calls register_trigger_from_self_skill_event
Relays-->>NH: EOSE
NH->>NH: on_self_skill_eose sets g_self_skill_eose_received=1
NH->>Main: calls on_self_skill_eose_load_triggers callback
Main->>TM: trigger_manager_load_from_skills<br/>reads ALL cached skills via<br/>nostr_handler_get_self_events_by_kind_json
Note over TM: For each skill with trigger+filter tags:<br/>calls trigger_manager_add
Main->>Main: nostr_handler_wait_for_self_skill_eose returns 0
Main->>Main: Startup step 14 passes
Main->>Main: Enter main poll loop
```
### Path 2: Live skill event (post-startup)
```mermaid
sequenceDiagram
participant Relays as Nostr Relays
participant NH as nostr_handler
participant TM as trigger_manager
Note over NH: Subscription stays open after EOSE
Relays-->>NH: New/updated skill EVENT arrives
NH->>NH: on_self_skill_event
NH->>NH: register_trigger_from_self_skill_event
Note over NH: Checks: kind 31123/31124?<br/>Has trigger+filter tags?<br/>Is adopted via kind 10123?
NH->>TM: trigger_manager_add or update
NH->>NH: self_skill_cache_upsert_event_locked
```
### Path 3: Cron polling in main loop
```mermaid
sequenceDiagram
participant Main as main loop
participant TM as trigger_manager
participant Agent as agent.c
loop Every iteration
Main->>TM: trigger_manager_poll
Note over TM: Throttled to once per 30 seconds
TM->>TM: For each trigger where type==CRON:<br/>1. Validate cron_expr<br/>2. Check cron_matches_now<br/>3. Check last_cron_fire dedup - 50s guard
TM->>Agent: execute_llm_action with synthetic cron event
end
```
---
## The Six Failure Points
I have identified six places where a cron trigger can silently fail to load or fire. They are ordered from most likely to least likely given the logs from laantungir.net.
### 1. EOSE timeout kills startup entirely
**Where:** [`main.c:14941515`](src/main.c:1494)
The agent waits 15 seconds for self-skill EOSE. If relays are slow or the connection is flaky, this times out and **the entire agent process exits**:
```c
const int self_skill_eose_timeout_ms = 15000;
if (nostr_handler_wait_for_self_skill_eose(self_skill_eose_timeout_ms) != 0) {
// ... prints error, cleans up, return 1
}
```
From [`debug.log`](debug.log) line 670:
```
[2026-04-06 16:16:49] [ERROR] startup checklist [14] Subscribe self-skill cache: failed
(self-skill EOSE not received within timeout)
```
**Impact:** If the agent crashes at startup, no triggers load at all. If systemd restarts it and it crashes again, you get a restart loop with zero cron execution.
### 2. Adoption check gates live trigger registration
**Where:** [`nostr_handler.c:1462`](src/nostr_handler.c:1462)
When a skill event arrives live via [`on_self_skill_event()`](src/nostr_handler.c:2428), it calls [`register_trigger_from_self_skill_event()`](src/nostr_handler.c:1418) which checks:
```c
if (!self_skill_is_adopted_local(kind_val, pubkey->valuestring, d_tag)) {
DEBUG_INFO("live self-skill trigger ignored (not adopted) d_tag=%s", d_tag);
return;
}
```
[`self_skill_is_adopted_local()`](src/nostr_handler.c:1366) scans `g_self_skill_events` for a kind 10123 event with an `["a", "31123:<pubkey>:<d_tag>"]` tag. If the adoption event hasn't arrived yet (race condition — skill event arrives before adoption event), the trigger is silently skipped.
**Critically:** The bulk load path in [`trigger_manager_load_from_skills()`](src/trigger_manager.c:818) does **NOT** check adoption at all — it loads every skill with trigger+filter tags. So the adoption gate only affects the live path.
### 3. Bulk load is one-shot with `loaded_once` guard
**Where:** [`main.c:42`](src/main.c:42)
```c
if (ctx->loaded_once) {
DEBUG_TRACE("deferred trigger load skipped (already loaded once)");
return;
}
```
The EOSE callback fires once, calls [`trigger_manager_load_from_skills()`](src/trigger_manager.c:818), sets `loaded_once = 1`, and never runs again. If a skill event arrives **after** EOSE but **before** the live path processes it, it falls into a gap:
- The bulk load already ran and won't run again
- The live [`register_trigger_from_self_skill_event()`](src/nostr_handler.c:1418) may reject it due to the adoption check
### 4. Cron expression validation rejects silently at add time
**Where:** [`trigger_manager.c:11061112`](src/trigger_manager.c:1106)
```c
if (t->trigger_type == TRIGGER_TYPE_CRON) {
if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
DEBUG_WARN("trigger add rejected: invalid cron expression d_tag=%s expr=%s", ...);
memset(t, 0, sizeof(*t));
return -1;
}
}
```
This does log a warning, but the caller in [`trigger_manager_load_from_skills()`](src/trigger_manager.c:896) just silently skips to the next skill. The `considered` count increments but `loaded` does not — the only evidence is in the log line `trigger manager loaded X trigger(s) from self skills (considered=Y)` where X < Y.
The existing plan in [`fix_cron_triggers_not_firing.md`](plans/fix_cron_triggers_not_firing.md) documents that a bare `*` was being used instead of `* * * * *`, and that shorthand expansion was added. This has been fixed.
### 5. Poll throttle delays first evaluation by up to 30 seconds
**Where:** [`trigger_manager.c:1395`](src/trigger_manager.c:1395)
```c
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 30) {
return 0;
}
```
After init, `last_poll_at` starts at 0 (fixed from the original bug where it was `time(NULL)`), so the first poll runs immediately. But subsequent polls are throttled to every 30 seconds. For a daily cron this is fine, but it means the cron match window is checked at most twice per minute.
### 6. Cron dedup guard can skip legitimate fires
**Where:** [`trigger_manager.c:1422`](src/trigger_manager.c:1422)
```c
if (t->last_cron_fire > 0 && (now - t->last_cron_fire) < 50) {
continue;
}
```
With a 30-second poll interval and a 50-second dedup window, a cron that matches at minute X will fire once and then be suppressed for the next poll. This is correct behavior for preventing double-fires within the same minute. But if the system clock jumps or NTP adjusts, this could cause unexpected skips.
---
## Robustness Suggestions
### A. Retry EOSE with backoff instead of hard abort
**Current:** EOSE timeout → process exit.
**Proposed:** Retry 23 times with increasing timeout (15s, 30s, 60s). If all retries fail, start the agent in degraded mode with only startup-config triggers active, and schedule a background re-attempt.
```mermaid
flowchart TD
SUB[Subscribe self-skills] --> WAIT{Wait for EOSE}
WAIT -->|Received| LOAD[Load triggers from skills]
WAIT -->|Timeout 15s| RETRY1{Retry 1 - 30s timeout}
RETRY1 -->|Received| LOAD
RETRY1 -->|Timeout| RETRY2{Retry 2 - 60s timeout}
RETRY2 -->|Received| LOAD
RETRY2 -->|Timeout| DEGRADED[Start in degraded mode<br/>startup triggers only<br/>schedule background retry]
LOAD --> READY[Agent READY]
DEGRADED --> READY
```
### B. Remove adoption gate from live trigger registration
The bulk load path doesn't check adoption. The live path does. This inconsistency means a skill created via `skill_create` with `auto_adopt=true` might not register its trigger if the adoption event arrives on a different relay or with different timing.
**Proposed:** Either:
1. Remove the adoption check from [`register_trigger_from_self_skill_event()`](src/nostr_handler.c:1462) to match the bulk path, OR
2. Add the adoption check to the bulk path too for consistency, but with a fallback: if adoption events haven't loaded yet, defer the check.
Option 1 is simpler and more robust. The agent already only subscribes to its own pubkey's events, so all skills in the cache are self-authored.
### C. Allow re-running bulk trigger load
Remove the `loaded_once` guard or replace it with a debounced re-load. When new skill events arrive after EOSE, schedule a re-scan of all cached skills after a short delay (e.g., 5 seconds). This closes the gap between EOSE and late-arriving events.
### D. Add a periodic trigger reconciliation
Every N minutes (e.g., 5), compare the set of cached skills that have trigger+filter tags against the active trigger list. Register any missing triggers. This is a safety net that catches any trigger that was missed during startup.
```c
// In trigger_manager_poll, after cron evaluation:
if (now - mgr->last_reconcile_at > 300) {
trigger_manager_load_from_skills(mgr); // idempotent via update path
mgr->last_reconcile_at = now;
}
```
### E. Log trigger registration summary at startup
After the deferred trigger load, log not just the count but also which trigger types were loaded:
```
trigger manager loaded 3 trigger(s): dm=2 cron=1 webhook=0 chain=0 nostr-sub=0
```
This makes it immediately obvious from the log whether a cron trigger was picked up.
### F. Add an HTTP API endpoint for trigger status
Expose `/api/triggers` that returns the same JSON as the `trigger_list` tool. This allows external monitoring (e.g., a health check script) to verify that expected cron triggers are loaded without going through the LLM.
---
## Summary of What Happened on laantungir.net
Based on the logs:
1. The agent started successfully multiple times (startup step 14 passed with `skills=3 adoptions=1` or similar).
2. The deferred trigger load status shows **only dm-type triggers** were loaded — no cron triggers appear in any log entry.
3. On 2026-04-06, the agent failed at startup step 14 with EOSE timeout, meaning it never even got to trigger loading.
4. There is **zero** evidence of `cron trigger matched` or `cron trigger skipped` in any log file, confirming the cron trigger was never in the active trigger set.
The most likely explanation: the cron skill event exists on relays but either:
- It was missing the `["trigger", "cron"]` and/or `["filter", "<expr>"]` tags when it was published
- It was not adopted (no kind 10123 event referencing it) and arrived via the live path where adoption is checked
- The cron expression was invalid and was silently rejected at add time
- The EOSE timeout prevented the agent from ever reaching the trigger loading phase
Running `trigger_list` on the live server will definitively answer which of these is the case.
+38 -83
View File
@@ -35,8 +35,6 @@ static pthread_mutex_t g_context_part_names_mutex = PTHREAD_MUTEX_INITIALIZER;
#define AGENT_DEBOUNCE_WINDOW_SECONDS 5
#define AGENT_DEBOUNCE_CACHE_SIZE 256
#define AGENT_HISTORY_TURNS 12
#define AGENT_HISTORY_QUERY_LIMIT 200
#define AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS 300
#define AGENT_ADOPTED_SKILLS_MAX 24
#define AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS 1800
@@ -681,7 +679,21 @@ static char* build_slash_help_all_json(void) {
int adopted_count = 0;
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
cJSON* ev0 = NULL;
double best_created = -1.0;
int an = cJSON_GetArraySize(adoption_events);
for (int ai = 0; ai < an; ai++) {
cJSON* ev = cJSON_GetArrayItem(adoption_events, ai);
if (!ev || !cJSON_IsObject(ev)) {
continue;
}
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
if (!ev0 || created > best_created) {
ev0 = ev;
best_created = created;
}
}
cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
if (tags && cJSON_IsArray(tags)) {
int tn = cJSON_GetArraySize(tags);
@@ -1048,73 +1060,6 @@ static __attribute__((unused)) char* build_sender_verification_text(didactyl_sen
}
static __attribute__((unused)) int append_recent_admin_dm_history(cJSON* messages, const char* current_message) {
if (!messages || !g_cfg) {
return -1;
}
char* history_json = nostr_handler_get_dm_history_json(g_cfg->admin.pubkey, AGENT_HISTORY_TURNS);
if (!history_json) {
return 0;
}
cJSON* items = cJSON_Parse(history_json);
free(history_json);
if (!items || !cJSON_IsArray(items)) {
cJSON_Delete(items);
return 0;
}
const char* last_appended_role = NULL;
const char* last_appended_content = NULL;
int n = cJSON_GetArraySize(items);
for (int i = 0; i < n; i++) {
cJSON* item = cJSON_GetArrayItem(items, i);
cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL;
cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL;
cJSON* created_at = item ? cJSON_GetObjectItemCaseSensitive(item, "created_at") : NULL;
if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) ||
!role->valuestring || !content->valuestring) {
continue;
}
const char* role_s = role->valuestring;
const char* content_s = content->valuestring;
int role_is_user = (strcmp(role_s, "user") == 0);
if (!role_is_user && strcmp(role_s, "assistant") != 0) {
continue;
}
if (i == n - 1 && role_is_user && current_message && strcmp(content_s, current_message) == 0) {
continue;
}
if (last_appended_role && last_appended_content &&
strcmp(last_appended_role, role_s) == 0 &&
strcmp(last_appended_content, content_s) == 0) {
continue;
}
if (append_simple_message(messages, role_s, content_s) != 0) {
cJSON_Delete(items);
return -1;
}
cJSON* appended = cJSON_GetArrayItem(messages, cJSON_GetArraySize(messages) - 1);
if (appended && cJSON_IsObject(appended) && created_at && cJSON_IsNumber(created_at)) {
cJSON_AddNumberToObject(appended, "_ts", created_at->valuedouble);
}
last_appended_role = role_s;
last_appended_content = content_s;
}
cJSON_Delete(items);
return 0;
}
static cJSON* find_tag_value_string_local(cJSON* tags, const char* key) {
if (!tags || !key || !cJSON_IsArray(tags)) {
return NULL;
@@ -1449,7 +1394,29 @@ static int refresh_adopted_skills_cache_if_needed(void) {
free(adoption_json);
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
cJSON* list_event = cJSON_GetArrayItem(adoption_events, 0);
cJSON* list_event = NULL;
double list_event_created_at = -1.0;
int adoption_n = cJSON_GetArraySize(adoption_events);
for (int ai = 0; ai < adoption_n; ai++) {
cJSON* ev = cJSON_GetArrayItem(adoption_events, ai);
if (!ev || !cJSON_IsObject(ev)) {
continue;
}
cJSON* ev_pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
strcmp(ev_pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
continue;
}
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
if (!list_event || created > list_event_created_at) {
list_event = ev;
list_event_created_at = created;
}
}
cJSON* list_tags = list_event ? cJSON_GetObjectItemCaseSensitive(list_event, "tags") : NULL;
if (list_tags && cJSON_IsArray(list_tags)) {
@@ -1761,18 +1728,6 @@ static __attribute__((unused)) int append_adopted_skills_context(cJSON* messages
return rc;
}
static __attribute__((unused)) cJSON* build_recent_admin_dm_history_messages(const char* current_message) {
cJSON* messages = cJSON_CreateArray();
if (!messages) {
return NULL;
}
if (append_recent_admin_dm_history(messages, current_message) != 0) {
cJSON_Delete(messages);
return NULL;
}
return messages;
}
int agent_init(didactyl_config_t* config) {
if (!config) {
return -1;
+15 -1
View File
@@ -815,7 +815,21 @@ static char* build_slash_help_all_json_local(void) {
int adopted_count = 0;
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
cJSON* ev0 = NULL;
double best_created = -1.0;
int an = cJSON_GetArraySize(adoption_events);
for (int ai = 0; ai < an; ai++) {
cJSON* ev = cJSON_GetArrayItem(adoption_events, ai);
if (!ev || !cJSON_IsObject(ev)) {
continue;
}
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
if (!ev0 || created > best_created) {
ev0 = ev;
best_created = created;
}
}
cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
if (tags && cJSON_IsArray(tags)) {
int tn = cJSON_GetArraySize(tags);
+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 2
#define DIDACTYL_VERSION_PATCH 38
#define DIDACTYL_VERSION "v0.2.38"
#define DIDACTYL_VERSION_PATCH 48
#define DIDACTYL_VERSION "v0.2.48"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+235 -7
View File
@@ -1510,6 +1510,7 @@ static void register_trigger_from_self_skill_event(cJSON* event) {
static void self_skill_cache_upsert_event_locked(cJSON* event) {
if (!event || !cJSON_IsObject(event)) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: event missing or not object");
return;
}
@@ -1517,34 +1518,51 @@ static void self_skill_cache_upsert_event_locked(cJSON* event) {
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
cJSON* ev_id = cJSON_GetObjectItemCaseSensitive(event, "id");
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1;
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
DEBUG_INFO("[didactyl] self-skill upsert candidate: kind=%d author=%.16s... id=%.16s... created_at=%.0f",
kind_val,
(pubkey && cJSON_IsString(pubkey) && pubkey->valuestring) ? pubkey->valuestring : "?",
(ev_id && cJSON_IsString(ev_id) && ev_id->valuestring) ? ev_id->valuestring : "?",
created);
if (!kind || !cJSON_IsNumber(kind) ||
!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
!tags || !cJSON_IsArray(tags)) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: missing required kind/pubkey/tags");
return;
}
int kind_val = (int)kind->valuedouble;
if (kind_val != 31123 && kind_val != 31124 && kind_val != 10123) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: kind not in {31123,31124,10123}");
return;
}
if (!g_cfg) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: config unavailable");
return;
}
int is_agent_author = (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0);
int is_admin_author = (g_cfg->admin.pubkey[0] != '\0' && strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0);
if (!is_agent_author && !is_admin_author) {
return;
if (kind_val == 10123) {
if (!is_agent_author) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: kind=10123 author is not self");
return;
}
}
if (is_admin_author && kind_val != 31123) {
if (is_admin_author && kind_val == 31124) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: admin cannot own private skill (31124)");
return;
}
const char* d_tag = find_tag_value_local(tags, "d");
if ((kind_val == 31123 || kind_val == 31124) && (!d_tag || d_tag[0] == '\0')) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: parameterized kind missing d-tag");
return;
}
@@ -1552,12 +1570,11 @@ static void self_skill_cache_upsert_event_locked(cJSON* event) {
cJSON_Delete(g_self_skill_events);
g_self_skill_events = cJSON_CreateArray();
if (!g_self_skill_events) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: failed to create cache array");
return;
}
}
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
int n = cJSON_GetArraySize(g_self_skill_events);
for (int i = 0; i < n; i++) {
cJSON* existing = cJSON_GetArrayItem(g_self_skill_events, i);
@@ -1589,23 +1606,56 @@ static void self_skill_cache_upsert_event_locked(cJSON* event) {
cJSON* ex_created_at = cJSON_GetObjectItemCaseSensitive(existing, "created_at");
double ex_created = (ex_created_at && cJSON_IsNumber(ex_created_at)) ? ex_created_at->valuedouble : 0.0;
if (created < ex_created) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: stale (existing created_at=%.0f >= new created_at=%.0f)",
ex_created,
created);
return;
}
cJSON* dup = cJSON_Duplicate(event, 1);
if (!dup) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: failed to duplicate event");
return;
}
cJSON_ReplaceItemInArray(g_self_skill_events, i, dup);
DEBUG_INFO("[didactyl] self-skill upsert accepted: replaced existing (old created_at=%.0f)", ex_created);
if (kind_val == 10123) {
int a_count = 0;
cJSON* t;
cJSON_ArrayForEach(t, tags) {
if (cJSON_IsArray(t) && cJSON_GetArraySize(t) >= 2) {
cJSON* t0 = cJSON_GetArrayItem(t, 0);
if (t0 && cJSON_IsString(t0) && t0->valuestring && strcmp(t0->valuestring, "a") == 0) {
a_count++;
}
}
}
DEBUG_INFO("[didactyl] kind 10123 accepted with %d 'a' tags", a_count);
}
return;
}
cJSON* dup = cJSON_Duplicate(event, 1);
if (!dup) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: failed to duplicate event");
return;
}
cJSON_AddItemToArray(g_self_skill_events, dup);
DEBUG_INFO("[didactyl] self-skill upsert accepted: new entry added");
if (kind_val == 10123) {
int a_count = 0;
cJSON* t;
cJSON_ArrayForEach(t, tags) {
if (cJSON_IsArray(t) && cJSON_GetArraySize(t) >= 2) {
cJSON* t0 = cJSON_GetArrayItem(t, 0);
if (t0 && cJSON_IsString(t0) && t0->valuestring && strcmp(t0->valuestring, "a") == 0) {
a_count++;
}
}
}
DEBUG_INFO("[didactyl] kind 10123 accepted with %d 'a' tags", a_count);
}
}
static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
@@ -2512,7 +2562,7 @@ int nostr_handler_init(didactyl_config_t* config) {
nostr_pool_reconnect_config_t reconnect = *nostr_pool_reconnect_config_default();
reconnect.enable_auto_reconnect = 1;
reconnect.ping_interval_seconds = 20;
reconnect.pong_timeout_seconds = 10;
reconnect.pong_timeout_seconds = 30;
g_pool = nostr_relay_pool_create(&reconnect);
if (!g_pool) {
@@ -2773,6 +2823,10 @@ int nostr_handler_subscribe_self_skills(void) {
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 300);
char* filter_str = cJSON_PrintUnformatted(filter);
DEBUG_INFO("[didactyl] self-skill subscription filter: %s", filter_str ? filter_str : "(null)");
free(filter_str);
pthread_mutex_lock(&g_subscription_mutex);
managed_subscriptions_init_defaults();
if (managed_subscription_register_locked(MANAGED_SUB_SELF_SKILLS,
@@ -3694,6 +3748,180 @@ char* nostr_handler_get_self_skill_events_json(void) {
return json ? json : strdup("[]");
}
int nostr_handler_refresh_self_skill_cache_from_relays(int timeout_ms) {
if (!g_cfg || !g_pool) {
return -1;
}
int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : 5000;
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_AddItemToArray(kinds, cJSON_CreateNumber(10123));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 500);
char* events_json = nostr_handler_query_json(filter, effective_timeout_ms);
cJSON_Delete(filter);
if (!events_json) {
return -1;
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
return -1;
}
cJSON* adopted_authors = cJSON_CreateObject();
if (adopted_authors) {
cJSON* latest_self_adoption = NULL;
double latest_self_adoption_created = -1.0;
int n0 = cJSON_GetArraySize(events);
for (int i = 0; i < n0; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
if (!ev || !cJSON_IsObject(ev)) {
continue;
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 10123 ||
!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
continue;
}
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
if (!latest_self_adoption || created > latest_self_adoption_created) {
latest_self_adoption = ev;
latest_self_adoption_created = created;
}
}
if (latest_self_adoption) {
cJSON* tags = cJSON_GetObjectItemCaseSensitive(latest_self_adoption, "tags");
if (tags && cJSON_IsArray(tags)) {
int tn = cJSON_GetArraySize(tags);
for (int ti = 0; ti < tn; ti++) {
cJSON* tag = cJSON_GetArrayItem(tags, ti);
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) || !k->valuestring ||
!cJSON_IsString(v) || !v->valuestring || strcmp(k->valuestring, "a") != 0) {
continue;
}
int adopted_kind = 0;
char adopted_pubkey[65] = {0};
char adopted_d_tag[65] = {0};
if (parse_skill_address_local(v->valuestring, &adopted_kind, adopted_pubkey, adopted_d_tag) != 0) {
continue;
}
if (adopted_pubkey[0] == '\0') {
continue;
}
if (!cJSON_GetObjectItemCaseSensitive(adopted_authors, adopted_pubkey)) {
cJSON_AddBoolToObject(adopted_authors, adopted_pubkey, 1);
}
}
}
}
int adopted_author_count = 0;
cJSON* adopted_it = NULL;
cJSON_ArrayForEach(adopted_it, adopted_authors) {
adopted_author_count++;
}
if (adopted_author_count > 0) {
cJSON* adopted_filter = cJSON_CreateObject();
cJSON* adopted_kinds = cJSON_CreateArray();
cJSON* adopted_author_arr = cJSON_CreateArray();
if (adopted_filter && adopted_kinds && adopted_author_arr) {
cJSON_AddItemToArray(adopted_kinds, cJSON_CreateNumber(31123));
cJSON_AddItemToArray(adopted_kinds, cJSON_CreateNumber(31124));
cJSON_AddItemToObject(adopted_filter, "kinds", adopted_kinds);
cJSON_ArrayForEach(adopted_it, adopted_authors) {
if (adopted_it->string && adopted_it->string[0] != '\0') {
cJSON_AddItemToArray(adopted_author_arr, cJSON_CreateString(adopted_it->string));
}
}
cJSON_AddItemToObject(adopted_filter, "authors", adopted_author_arr);
cJSON_AddNumberToObject(adopted_filter, "limit", 1000);
char* adopted_json = nostr_handler_query_json(adopted_filter, effective_timeout_ms);
if (adopted_json) {
cJSON* adopted_events = cJSON_Parse(adopted_json);
free(adopted_json);
if (adopted_events && cJSON_IsArray(adopted_events)) {
int an = cJSON_GetArraySize(adopted_events);
for (int ai = 0; ai < an; ai++) {
cJSON* ev = cJSON_GetArrayItem(adopted_events, ai);
cJSON* dup = ev ? cJSON_Duplicate(ev, 1) : NULL;
if (dup) {
cJSON_AddItemToArray(events, dup);
}
}
}
cJSON_Delete(adopted_events);
}
} else {
cJSON_Delete(adopted_filter);
cJSON_Delete(adopted_kinds);
cJSON_Delete(adopted_author_arr);
}
cJSON_Delete(adopted_filter);
}
}
cJSON_Delete(adopted_authors);
int merged = 0;
pthread_mutex_lock(&g_self_skill_mutex);
int n = cJSON_GetArraySize(events);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
if (!ev || !cJSON_IsObject(ev)) {
continue;
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1;
if (kind_val == 31124) {
(void)normalize_private_skill_event_local(ev);
}
self_skill_cache_upsert_event_locked(ev);
merged++;
}
pthread_mutex_unlock(&g_self_skill_mutex);
cJSON_Delete(events);
agent_invalidate_adopted_skills_cache();
DEBUG_INFO("[didactyl] skill_refresh relay reload merged %d self-skill/adoption events", merged);
return 0;
}
int nostr_handler_is_event_in_self_cache(const char* event_id_hex) {
if (!event_id_hex || strlen(event_id_hex) != 64U) {
return 0;
+1
View File
@@ -77,6 +77,7 @@ int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adopt
void nostr_handler_refresh_relay_statuses(void);
int nostr_handler_sync_relays_from_config(void);
char* nostr_handler_get_self_skill_events_json(void);
int nostr_handler_refresh_self_skill_cache_from_relays(int timeout_ms);
int nostr_handler_is_event_in_self_cache(const char* event_id_hex);
const char* nostr_handler_get_startup_display_name(void);
int nostr_handler_connected_relay_count(void);
+164 -45
View File
@@ -3,6 +3,8 @@
#include "tools_internal.h"
#include <ctype.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -131,6 +133,52 @@ static cJSON* parse_args_local(const char* args_json) {
return args;
}
static int append_textf_skill_list_local(char** out, size_t* cap, size_t* used, const char* fmt, ...) {
if (!out || !cap || !used || !fmt) {
return -1;
}
va_list ap;
va_start(ap, fmt);
va_list ap2;
va_copy(ap2, ap);
int needed = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (needed < 0) {
va_end(ap2);
return -1;
}
size_t need = (size_t)needed;
if (*used + need + 1U > *cap) {
size_t new_cap = (*cap > 0U) ? *cap : 256U;
while (*used + need + 1U > new_cap) {
if (new_cap > (SIZE_MAX / 2U)) {
va_end(ap2);
return -1;
}
new_cap *= 2U;
}
char* grown = (char*)realloc(*out, new_cap);
if (!grown) {
va_end(ap2);
return -1;
}
*out = grown;
*cap = new_cap;
}
int wrote = vsnprintf((*out) + (*used), *cap - *used, fmt, ap2);
va_end(ap2);
if (wrote < 0) {
return -1;
}
*used += (size_t)wrote;
return 0;
}
static int is_hex_string_len_local(const char* s, size_t expected_len) {
if (!s || strlen(s) != expected_len) return 0;
for (size_t i = 0; i < expected_len; i++) {
@@ -333,9 +381,31 @@ static int fetch_adoption_list_tags_local(tools_context_t* ctx, cJSON** out_tags
free(events_json);
if (events && cJSON_IsArray(events) && cJSON_GetArraySize(events) > 0) {
cJSON* ev0 = cJSON_GetArrayItem(events, 0);
if (ev0 && cJSON_IsObject(ev0)) {
cJSON* ev_content = cJSON_GetObjectItemCaseSensitive(ev0, "content");
cJSON* best = NULL;
double best_created = -1.0;
int n = cJSON_GetArraySize(events);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
if (!ev || !cJSON_IsObject(ev)) {
continue;
}
cJSON* ev_pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
strcmp(ev_pubkey->valuestring, ctx->cfg->keys.public_key_hex) != 0) {
continue;
}
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
if (!best || created > best_created) {
best = ev;
best_created = created;
}
}
if (best && cJSON_IsObject(best)) {
cJSON* ev_content = cJSON_GetObjectItemCaseSensitive(best, "content");
if (ev_content && cJSON_IsString(ev_content) && ev_content->valuestring) {
free(content);
content = strdup(ev_content->valuestring);
@@ -346,7 +416,7 @@ static int fetch_adoption_list_tags_local(tools_context_t* ctx, cJSON** out_tags
}
}
cJSON* ev_tags = cJSON_GetObjectItemCaseSensitive(ev0, "tags");
cJSON* ev_tags = cJSON_GetObjectItemCaseSensitive(best, "tags");
if (ev_tags && cJSON_IsArray(ev_tags)) {
cJSON* dup = cJSON_Duplicate(ev_tags, 1);
if (!dup) {
@@ -1170,11 +1240,9 @@ char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
}
cJSON* out = cJSON_CreateObject();
cJSON* summary_rows = cJSON_CreateArray();
cJSON* skills = cJSON_CreateArray();
if (!out || !summary_rows || !skills) {
if (!out || !skills) {
cJSON_Delete(out);
cJSON_Delete(summary_rows);
cJSON_Delete(skills);
cJSON_Delete(events);
cJSON_Delete(adoption_tags);
@@ -1182,7 +1250,21 @@ char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
return NULL;
}
size_t rendered_cap = 512U;
size_t rendered_used = 0U;
char* rendered_lines = (char*)malloc(rendered_cap);
if (!rendered_lines) {
cJSON_Delete(out);
cJSON_Delete(skills);
cJSON_Delete(events);
cJSON_Delete(adoption_tags);
free(adoption_content);
return NULL;
}
rendered_lines[0] = '\0';
int adopted_count = 0;
int skill_count = 0;
int event_count = cJSON_GetArraySize(events);
for (int i = 0; i < event_count; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
@@ -1214,59 +1296,91 @@ char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
if (adopted_filter >= 0 && is_adopted != adopted_filter) {
continue;
}
const char* owner = skill_owner_label_local(ctx->cfg, pubkey->valuestring);
cJSON* scope_tag = find_tag_value_string_local(tags, "scope");
cJSON* desc_tag = find_tag_value_string_local(tags, "description");
const char* scope_out = (scope_tag && cJSON_IsString(scope_tag) && scope_tag->valuestring)
? scope_tag->valuestring
: ((kind_val == 31124) ? "private" : "public");
const char* desc_out = (desc_tag && cJSON_IsString(desc_tag) && desc_tag->valuestring)
? desc_tag->valuestring
: "";
cJSON* row = cJSON_CreateObject();
if (!row) {
continue;
}
cJSON_AddStringToObject(row, "d_tag", d->valuestring);
cJSON_AddStringToObject(row, "owner", owner);
cJSON_AddStringToObject(row, "scope", scope_out);
cJSON_AddNumberToObject(row, "kind", kind_val);
cJSON_AddBoolToObject(row, "adopted", is_adopted ? 1 : 0);
if (desc_out[0] != '\0') {
cJSON_AddStringToObject(row, "description", desc_out);
}
cJSON_AddItemToArray(skills, row);
if (is_adopted) {
adopted_count++;
}
skill_count++;
const char* owner = skill_owner_label_local(ctx->cfg, pubkey->valuestring);
cJSON* detail = extract_skill_summary_local(ev);
if (detail) {
cJSON_AddStringToObject(detail, "owner", owner);
cJSON_AddBoolToObject(detail, "adopted", is_adopted ? 1 : 0);
cJSON_AddItemToArray(skills, detail);
}
cJSON* row = cJSON_CreateObject();
if (row) {
cJSON_AddStringToObject(row, "d_tag", d->valuestring);
cJSON_AddStringToObject(row, "owner", owner);
cJSON_AddBoolToObject(row, "adopted", is_adopted ? 1 : 0);
cJSON_AddItemToArray(summary_rows, row);
if (append_textf_skill_list_local(&rendered_lines,
&rendered_cap,
&rendered_used,
"- %s | owner=%s | scope=%s | adopted=%s%s%s\n",
d->valuestring,
owner,
scope_out,
is_adopted ? "yes" : "no",
desc_out[0] ? " | description=" : "",
desc_out) != 0) {
free(rendered_lines);
cJSON_Delete(out);
cJSON_Delete(skills);
cJSON_Delete(events);
cJSON_Delete(adoption_tags);
free(adoption_content);
return NULL;
}
}
cJSON* skills_pretty = cJSON_CreateArray();
if (!skills_pretty) {
if (skill_count == 0) {
if (append_textf_skill_list_local(&rendered_lines, &rendered_cap, &rendered_used, "- (none)\n") != 0) {
free(rendered_lines);
cJSON_Delete(out);
cJSON_Delete(skills);
cJSON_Delete(events);
cJSON_Delete(adoption_tags);
free(adoption_content);
return NULL;
}
}
char header[128];
snprintf(header, sizeof(header), "Skills: %d total (%d adopted)\n", skill_count, adopted_count);
size_t header_len = strlen(header);
char* rendered = (char*)malloc(header_len + rendered_used + 1U);
if (!rendered) {
free(rendered_lines);
cJSON_Delete(out);
cJSON_Delete(skills);
cJSON_Delete(events);
cJSON_Delete(adoption_tags);
free(adoption_content);
return NULL;
}
int skill_count = cJSON_GetArraySize(skills);
for (int i = 0; i < skill_count; i++) {
cJSON* skill_item = cJSON_GetArrayItem(skills, i);
if (!skill_item) continue;
char* skill_json = cJSON_PrintUnformatted(skill_item);
if (!skill_json) continue;
cJSON* block = cJSON_CreateString(skill_json);
free(skill_json);
if (block) {
cJSON_AddItemToArray(skills_pretty, block);
}
}
memcpy(rendered, header, header_len);
memcpy(rendered + header_len, rendered_lines, rendered_used + 1U);
free(rendered_lines);
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddBoolToObject(out, "fallback_used", 0);
cJSON_AddStringToObject(out, "skills_separator", "\n\n\n");
cJSON_AddItemToObject(out, "summary", summary_rows);
cJSON_AddItemToObject(out, "skills", skills);
cJSON_AddItemToObject(out, "skills_pretty", skills_pretty);
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(skills));
cJSON_AddNumberToObject(out, "count", skill_count);
cJSON_AddNumberToObject(out, "adopted_count", adopted_count);
cJSON_AddItemToObject(out, "skills", skills);
cJSON_AddStringToObject(out, "rendered", rendered);
free(rendered);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -2458,12 +2572,17 @@ char* execute_skill_refresh(tools_context_t* ctx, const char* args_json) {
(void)args_json;
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
int rc = nostr_handler_refresh_self_skill_cache_from_relays(5000);
if (rc != 0) {
return json_error_local("skill_refresh failed to reload self-skill cache from relays");
}
agent_invalidate_adopted_skills_cache();
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "message", "adopted skills cache invalidated; next context build will reload skills");
cJSON_AddStringToObject(out, "message", "self-skill cache refreshed from relays; adopted skills cache invalidated; next context build will use latest relay data");
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
+132
View File
@@ -589,6 +589,126 @@ static void parse_trigger_runtime_tags(cJSON* tags,
}
}
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;
@@ -810,6 +930,7 @@ int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg) {
}
mgr->last_poll_at = 0;
mgr->last_reconcile_at = 0;
DEBUG_INFO("[didactyl] trigger manager initialized (capacity=%d)", mgr->capacity);
return 0;
@@ -1447,6 +1568,17 @@ int trigger_manager_poll(trigger_manager_t* mgr) {
}
pthread_mutex_unlock(&mgr->mutex);
/*
* Temporarily disable periodic relay-backed trigger reconciliation.
* Keep code path in place for quick re-enable after adoption/cache fix validation.
*/
int enable_periodic_reconcile = 0;
if (enable_periodic_reconcile &&
(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;
}
+1
View File
@@ -57,6 +57,7 @@ typedef struct trigger_manager {
didactyl_config_t* cfg;
pthread_mutex_t mutex;
time_t last_poll_at;
time_t last_reconcile_at;
} trigger_manager_t;
int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg);