v0.0.42 - Add self-skill subscription cache and make skill_list read from cache

This commit is contained in:
Your Name
2026-03-05 17:33:37 -04:00
parent 2a71746a09
commit 36e4998e85
6 changed files with 234 additions and 67 deletions
+2 -2
View File
@@ -51,11 +51,11 @@ Agents learn capabilities through skills — Nostr events that any agent can di
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
## Current Status — v0.0.41
## Current Status — v0.0.42
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.41 — Fix startup relay-state log snapshot before main loop
> Last release update: v0.0.42 — Add self-skill subscription cache and make skill_list read from cache
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+6
View File
@@ -242,6 +242,12 @@ int main(int argc, char** argv) {
}
DEBUG_INFO("[didactyl] startup phase: subscribe admin context end");
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin");
if (nostr_handler_subscribe_self_skills() != 0) {
DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)");
}
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
char startup_dm[768];
const char* startup_name = nostr_handler_get_startup_display_name();
int connected_relays = nostr_handler_connected_relay_count();
+2 -2
View File
@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 0
#define DIDACTYL_VERSION_PATCH 41
#define DIDACTYL_VERSION "v0.0.41"
#define DIDACTYL_VERSION_PATCH 42
#define DIDACTYL_VERSION "v0.0.42"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+211
View File
@@ -43,6 +43,9 @@ static int g_admin_kind1_note_count = 0;
static pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER;
static cJSON* g_self_skill_events = NULL;
static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
#define DM_DEDUP_CACHE_SIZE 256
static char g_seen_dm_ids[DM_DEDUP_CACHE_SIZE][65];
@@ -207,6 +210,7 @@ static void upsert_kind1_note(time_t created_at, const char* content);
static int startup_self_kind1_exists(void);
static void load_startup_display_name(void);
static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback);
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
static void log_relay_state_changes(void) {
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
@@ -353,6 +357,120 @@ static void build_startup_kind1_content(char* out, size_t out_size, const char*
}
}
static const char* find_tag_value_local(cJSON* tags, const char* key) {
if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') {
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) || !k->valuestring ||
!cJSON_IsString(v) || !v->valuestring) {
continue;
}
if (strcmp(k->valuestring, key) == 0) {
return v->valuestring;
}
}
return NULL;
}
static void self_skill_cache_upsert_event_locked(cJSON* event) {
if (!event || !cJSON_IsObject(event)) {
return;
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
if (!kind || !cJSON_IsNumber(kind) ||
!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
!tags || !cJSON_IsArray(tags)) {
return;
}
int kind_val = (int)kind->valuedouble;
if (kind_val != 31123 && kind_val != 31124) {
return;
}
if (!g_cfg || strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
return;
}
const char* d_tag = find_tag_value_local(tags, "d");
if (!d_tag || d_tag[0] == '\0') {
return;
}
if (!g_self_skill_events || !cJSON_IsArray(g_self_skill_events)) {
cJSON_Delete(g_self_skill_events);
g_self_skill_events = cJSON_CreateArray();
if (!g_self_skill_events) {
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);
if (!existing || !cJSON_IsObject(existing)) {
continue;
}
cJSON* ex_kind = cJSON_GetObjectItemCaseSensitive(existing, "kind");
cJSON* ex_pubkey = cJSON_GetObjectItemCaseSensitive(existing, "pubkey");
cJSON* ex_tags = cJSON_GetObjectItemCaseSensitive(existing, "tags");
if (!ex_kind || !cJSON_IsNumber(ex_kind) ||
!ex_pubkey || !cJSON_IsString(ex_pubkey) || !ex_pubkey->valuestring ||
!ex_tags || !cJSON_IsArray(ex_tags)) {
continue;
}
const char* ex_d = find_tag_value_local(ex_tags, "d");
if (!ex_d || strcmp(ex_d, d_tag) != 0) {
continue;
}
if ((int)ex_kind->valuedouble != kind_val || strcmp(ex_pubkey->valuestring, pubkey->valuestring) != 0) {
continue;
}
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) {
return;
}
cJSON* dup = cJSON_Duplicate(event, 1);
if (!dup) {
return;
}
cJSON_ReplaceItemInArray(g_self_skill_events, i, dup);
return;
}
cJSON* dup = cJSON_Duplicate(event, 1);
if (!dup) {
return;
}
cJSON_AddItemToArray(g_self_skill_events, dup);
}
static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
if (!hex || !out_pubkey || strlen(hex) != 64U) {
return -1;
@@ -956,6 +1074,23 @@ static void on_admin_context_event(cJSON* event, const char* relay_url, void* us
pthread_mutex_unlock(&g_admin_ctx_mutex);
}
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) {
(void)relay_url;
(void)user_data;
if (!event || !g_cfg) {
return;
}
if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) {
return;
}
pthread_mutex_lock(&g_self_skill_mutex);
self_skill_cache_upsert_event_locked(event);
pthread_mutex_unlock(&g_self_skill_mutex);
}
int nostr_handler_init(didactyl_config_t* config) {
if (!config) {
return -1;
@@ -1006,6 +1141,14 @@ int nostr_handler_init(didactyl_config_t* config) {
g_startup_published = NULL;
g_startup_publish_tracking_enabled = 0;
pthread_mutex_lock(&g_self_skill_mutex);
cJSON_Delete(g_self_skill_events);
g_self_skill_events = cJSON_CreateArray();
pthread_mutex_unlock(&g_self_skill_mutex);
if (!g_self_skill_events) {
return -1;
}
return 0;
}
@@ -1107,6 +1250,51 @@ int nostr_handler_subscribe_admin_context(void) {
return rc;
}
int nostr_handler_subscribe_self_skills(void) {
if (!g_cfg || !g_pool) {
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(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 300);
nostr_pool_subscription_t* skill_sub = nostr_relay_pool_subscribe(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
filter,
on_self_skill_event,
on_eose,
NULL,
0,
0,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
cJSON_Delete(filter);
if (!skill_sub) {
return -1;
}
DEBUG_INFO("[didactyl] self-skill subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex);
return 0;
}
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
if (!g_cfg || !g_pool || !callback) {
return -1;
@@ -1594,6 +1782,24 @@ const char* nostr_handler_get_system_context(void) {
return g_system_context;
}
char* nostr_handler_get_self_skill_events_json(void) {
cJSON* dup = NULL;
pthread_mutex_lock(&g_self_skill_mutex);
if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
dup = cJSON_Duplicate(g_self_skill_events, 1);
}
pthread_mutex_unlock(&g_self_skill_mutex);
if (!dup) {
return strdup("[]");
}
char* out = cJSON_PrintUnformatted(dup);
cJSON_Delete(dup);
return out;
}
const char* nostr_handler_get_startup_display_name(void) {
return g_startup_display_name;
}
@@ -2069,4 +2275,9 @@ void nostr_handler_cleanup(void) {
pthread_mutex_lock(&g_admin_ctx_mutex);
free_admin_context_locked();
pthread_mutex_unlock(&g_admin_ctx_mutex);
pthread_mutex_lock(&g_self_skill_mutex);
cJSON_Delete(g_self_skill_events);
g_self_skill_events = NULL;
pthread_mutex_unlock(&g_self_skill_mutex);
}
+2
View File
@@ -29,6 +29,7 @@ typedef struct {
int nostr_handler_init(didactyl_config_t* config);
int nostr_handler_subscribe_admin_context(void);
int nostr_handler_subscribe_self_skills(void);
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data);
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message);
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message);
@@ -39,6 +40,7 @@ int nostr_handler_poll(int timeout_ms);
int nostr_handler_reconcile_startup_events(void);
void nostr_handler_refresh_relay_statuses(void);
const char* nostr_handler_get_system_context(void);
char* nostr_handler_get_self_skill_events_json(void);
const char* nostr_handler_get_startup_display_name(void);
int nostr_handler_connected_relay_count(void);
char* nostr_handler_get_admin_kind0_context(void);
+11 -63
View File
@@ -3569,69 +3569,16 @@ static char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
return json_error("skill_list scope must be public or private");
}
}
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);
cJSON_Delete(args);
return json_error("failed to create skill_list filter");
}
if (include_public) cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123));
if (include_private) cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->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);
cJSON_Delete(args);
if (!events_json) return json_error("skill_list query failed");
char* events_json = nostr_handler_get_self_skill_events_json();
if (!events_json) return json_error("skill_list cache unavailable");
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
return json_error("skill_list returned invalid JSON");
}
int fallback_used = 0;
if (cJSON_GetArraySize(events) == 0) {
cJSON_Delete(events);
events = NULL;
cJSON* fallback_filter = cJSON_CreateObject();
cJSON* fallback_kinds = cJSON_CreateArray();
if (!fallback_filter || !fallback_kinds) {
cJSON_Delete(fallback_filter);
cJSON_Delete(fallback_kinds);
return json_error("failed to create fallback skill_list filter");
}
if (include_public) cJSON_AddItemToArray(fallback_kinds, cJSON_CreateNumber(31123));
if (include_private) cJSON_AddItemToArray(fallback_kinds, cJSON_CreateNumber(31124));
cJSON_AddItemToObject(fallback_filter, "kinds", fallback_kinds);
cJSON_AddNumberToObject(fallback_filter, "limit", 600);
char* fallback_json = nostr_handler_query_json(fallback_filter, 8000);
cJSON_Delete(fallback_filter);
if (!fallback_json) {
return json_error("skill_list fallback query failed");
}
events = cJSON_Parse(fallback_json);
free(fallback_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
return json_error("skill_list fallback returned invalid JSON");
}
fallback_used = 1;
return json_error("skill_list cache returned invalid JSON");
}
cJSON* out = cJSON_CreateObject();
@@ -3648,12 +3595,13 @@ static char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
cJSON* ev = cJSON_GetArrayItem(events, i);
if (!ev || !cJSON_IsObject(ev)) continue;
if (fallback_used) {
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* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
if (!kind || !cJSON_IsNumber(kind)) {
continue;
}
int kind_val = (int)kind->valuedouble;
if ((kind_val == 31123 && !include_public) || (kind_val == 31124 && !include_private)) {
continue;
}
cJSON* summary = extract_skill_summary(ev);
@@ -3661,7 +3609,7 @@ static char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddBoolToObject(out, "fallback_used", fallback_used ? 1 : 0);
cJSON_AddBoolToObject(out, "fallback_used", 0);
cJSON_AddItemToObject(out, "skills", skills);
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(skills));