v0.0.42 - Add self-skill subscription cache and make skill_list read from cache
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user