Compare commits

...

3 Commits

4 changed files with 304 additions and 53 deletions

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.44
## Current Status — v0.2.47
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.2.44Fix adoption list selection to self-authored latest event in agent and skill tool
> Last release update: v0.2.47nostr: exclude admin from kind 10123 self-skill adoption and add self-skill upsert debug logs
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected

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 44
#define DIDACTYL_VERSION "v0.2.44"
#define DIDACTYL_VERSION_PATCH 47
#define DIDACTYL_VERSION "v0.2.47"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"

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]) {
@@ -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,
@@ -3717,9 +3771,6 @@ int nostr_handler_refresh_self_skill_cache_from_relays(int timeout_ms) {
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
if (g_cfg->admin.pubkey[0] != '\0' && strcmp(g_cfg->admin.pubkey, g_cfg->keys.public_key_hex) != 0) {
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->admin.pubkey));
}
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 500);
@@ -3736,6 +3787,114 @@ int nostr_handler_refresh_self_skill_cache_from_relays(int timeout_ms) {
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);

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++) {
@@ -1192,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);
@@ -1204,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);
@@ -1236,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);