Compare commits

...
9 Commits
8 changed files with 3794 additions and 146 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.35
## Current Status — v0.0.41
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.35Improve NIP-17 send reliability with re-entrancy guard, auth handshake retry window, and send-path debug logs
> Last release update: v0.0.41Fix startup relay-state log snapshot before main loop
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+3722
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -35,7 +35,7 @@ static pthread_mutex_t g_context_part_names_mutex = PTHREAD_MUTEX_INITIALIZER;
#define AGENT_DEBOUNCE_CACHE_SIZE 256
#define AGENT_HISTORY_TURNS 12
#define AGENT_HISTORY_QUERY_LIMIT 200
#define AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS 30
#define AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS 300
#define AGENT_ADOPTED_SKILLS_MAX 24
#define AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS 1800
#define AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS 7200
@@ -1415,7 +1415,7 @@ static int append_recent_admin_dm_history(cJSON* messages, const char* current_m
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", AGENT_HISTORY_QUERY_LIMIT);
char* events_json = nostr_handler_query_json(filter, 8000);
char* events_json = nostr_handler_query_json(filter, 1500);
cJSON_Delete(filter);
if (!events_json) {
return 0;
@@ -1648,7 +1648,7 @@ static int refresh_adopted_skills_cache_if_needed(void) {
cJSON_AddItemToObject(adopt_filter, "authors", authors);
cJSON_AddNumberToObject(adopt_filter, "limit", 1);
char* adoption_json = nostr_handler_query_json(adopt_filter, 8000);
char* adoption_json = nostr_handler_query_json(adopt_filter, 2000);
cJSON_Delete(adopt_filter);
cJSON* adoption_events = adoption_json ? cJSON_Parse(adoption_json) : NULL;
@@ -1700,7 +1700,7 @@ static int refresh_adopted_skills_cache_if_needed(void) {
cJSON_AddItemToObject(skill_filter, "#d", d_values);
cJSON_AddNumberToObject(skill_filter, "limit", 1);
char* skill_json = nostr_handler_query_json(skill_filter, 8000);
char* skill_json = nostr_handler_query_json(skill_filter, 2000);
cJSON_Delete(skill_filter);
if (!skill_json) {
continue;
+2
View File
@@ -318,6 +318,8 @@ int main(int argc, char** argv) {
DEBUG_INFO("[didactyl] HTTP API disabled (set api.enabled=true in config to enable)");
}
nostr_handler_refresh_relay_statuses();
DEBUG_INFO("[didactyl] entering main poll loop");
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
+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 35
#define DIDACTYL_VERSION "v0.0.35"
#define DIDACTYL_VERSION_PATCH 41
#define DIDACTYL_VERSION "v0.0.41"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+57 -134
View File
@@ -218,10 +218,19 @@ static void log_relay_state_changes(void) {
nostr_pool_relay_status_t now = nostr_relay_pool_get_relay_status(g_pool, relay);
nostr_pool_relay_status_t prev = g_last_relay_statuses[i];
if (now != prev) {
DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s",
relay,
relay_status_str(prev),
relay_status_str(now));
const char* conn_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, relay);
if (conn_err && (now == NOSTR_POOL_RELAY_DISCONNECTED || now == NOSTR_POOL_RELAY_ERROR)) {
DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s (cause: %s)",
relay,
relay_status_str(prev),
relay_status_str(now),
conn_err);
} else {
DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s",
relay,
relay_status_str(prev),
relay_status_str(now));
}
g_last_relay_statuses[i] = now;
if (now == NOSTR_POOL_RELAY_CONNECTED) {
@@ -231,6 +240,16 @@ static void log_relay_state_changes(void) {
}
}
void nostr_handler_refresh_relay_statuses(void) {
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
return;
}
for (int i = 0; i < g_cfg->relay_count; i++) {
g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]);
}
}
static void log_publish_targets(const char* action) {
if (!g_cfg) {
return;
@@ -1139,7 +1158,7 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
on_eose,
NULL,
0,
1,
0,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
@@ -1190,7 +1209,7 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
on_eose,
NULL,
0,
1,
0,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
@@ -1269,21 +1288,6 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
}
}
int* pre_publish_ok = NULL;
if (connected_count > 0) {
pre_publish_ok = (int*)calloc((size_t)connected_count, sizeof(int));
if (!pre_publish_ok) {
free(connected_relays);
cJSON_Delete(event);
return -1;
}
for (int i = 0; i < connected_count; i++) {
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, connected_relays[i]);
pre_publish_ok[i] = stats ? stats->events_published_ok : 0;
}
}
int sent = 0;
if (connected_count > 0) {
sent = nostr_relay_pool_publish_async(
@@ -1297,48 +1301,6 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
for (int i = 0; i < connected_count; i++) {
DEBUG_INFO("[didactyl] kind 4 event published to %s (async)", connected_relays[i]);
}
// Briefly drain relay messages so NIP-42 AUTH handshake can complete.
for (int i = 0; i < 5; i++) {
(void)nostr_relay_pool_poll(g_pool, 100);
}
int any_publish_ok = 0;
int auth_required_seen = 0;
for (int i = 0; i < connected_count; i++) {
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, connected_relays[i]);
if (stats && stats->events_published_ok > pre_publish_ok[i]) {
any_publish_ok = 1;
}
const char* pub_err = nostr_relay_pool_get_relay_last_publish_error(g_pool, connected_relays[i]);
if (pub_err && strstr(pub_err, "auth-required") != NULL) {
auth_required_seen = 1;
}
}
// Always retry this same signed event once after handshake drain.
// Some relays issue AUTH challenge for the first EVENT and only accept
// the subsequent resend after AUTH succeeds.
if (sent > 0) {
if (auth_required_seen || !any_publish_ok) {
DEBUG_WARN("[didactyl] retrying kind 4 event once after auth handshake window");
}
int resent = nostr_relay_pool_publish_async(
g_pool,
connected_relays,
connected_count,
event,
NULL,
NULL);
if (resent > sent) {
sent = resent;
}
for (int i = 0; i < 5; i++) {
(void)nostr_relay_pool_poll(g_pool, 100);
}
}
} else {
DEBUG_WARN("[didactyl] kind 4 event not queued: no connected relays");
}
@@ -1351,7 +1313,6 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
recipient_pubkey_hex,
sent);
free(pre_publish_ok);
free(connected_relays);
cJSON_Delete(event);
return sent > 0 ? 0 : -1;
@@ -1872,8 +1833,6 @@ char* nostr_handler_relay_info_json(const char* relay_url) {
}
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) {
static int g_nip17_send_in_progress = 0;
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') {
return -1;
}
@@ -1928,8 +1887,6 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me
int any_sent = 0;
int sent_count = 0;
g_nip17_send_in_progress++;
for (int i = 0; i < gift_count; i++) {
if (!gift_wraps[i]) {
continue;
@@ -1953,72 +1910,9 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me
cJSON_Delete(gift_wraps[i]);
}
if (g_nip17_send_in_progress == 1) {
for (int i = 0; i < 5; i++) {
(void)nostr_relay_pool_poll(g_pool, 100);
}
for (int i = 0; i < gift_count; i++) {
if (!gift_wraps[i]) {
continue;
}
}
int resend_total = 0;
for (int i = 0; i < gift_count; i++) {
(void)i;
}
// Recreate and resend once after auth handshake drain.
cJSON* retry_chat_event = nostr_nip17_create_chat_event(
message,
recipients,
1,
(subject && subject[0] != '\0') ? subject : NULL,
NULL,
NULL,
g_cfg->keys.public_key_hex);
if (retry_chat_event) {
cJSON* retry_gift_wraps[NIP17_MAX_GIFT_WRAPS] = {0};
int retry_count = nostr_nip17_send_dm(retry_chat_event,
recipients,
1,
g_cfg->keys.private_key,
retry_gift_wraps,
NIP17_MAX_GIFT_WRAPS,
0);
cJSON_Delete(retry_chat_event);
if (retry_count > 0) {
for (int i = 0; i < retry_count; i++) {
if (!retry_gift_wraps[i]) {
continue;
}
int resent = nostr_relay_pool_publish_async(g_pool,
target_relays,
target_count,
retry_gift_wraps[i],
NULL,
NULL);
if (resent > 0) {
any_sent = 1;
resend_total += resent;
}
cJSON_Delete(retry_gift_wraps[i]);
}
}
}
DEBUG_TRACE("[didactyl] NIP-17 send retry window complete: recipient=%.16s... initial_sent=%d retry_sent=%d",
recipient_pubkey_hex,
sent_count,
resend_total);
} else {
DEBUG_TRACE("[didactyl] NIP-17 send in nested context (depth=%d), skipping poll+retry window",
g_nip17_send_in_progress);
}
g_nip17_send_in_progress--;
DEBUG_TRACE("[didactyl] NIP-17 send complete: recipient=%.16s... sent_count=%d",
recipient_pubkey_hex,
sent_count);
if (!any_sent) {
DEBUG_WARN("[didactyl] NIP-17 send failed: no relay accepted publish for recipient %.16s...",
@@ -2108,9 +2002,38 @@ int nostr_handler_poll(int timeout_ms) {
return -1;
}
double start_ms = -1.0;
struct timespec ts_start;
if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == 0) {
start_ms = ts_start.tv_sec * 1000.0 + ts_start.tv_nsec / 1000000.0;
}
int rc = nostr_relay_pool_poll(g_pool, timeout_ms);
g_poll_counter++;
if (start_ms >= 0.0) {
struct timespec ts_end;
if (clock_gettime(CLOCK_MONOTONIC, &ts_end) == 0) {
const double end_ms = ts_end.tv_sec * 1000.0 + ts_end.tv_nsec / 1000000.0;
const double elapsed_ms = end_ms - start_ms;
const double expected_ms = timeout_ms > 0 ? (double)timeout_ms : 0.0;
if (elapsed_ms > expected_ms + 250.0) {
DEBUG_WARN("[didactyl] poll latency spike: nostr_relay_pool_poll(timeout=%d) took %.1fms rc=%d count=%llu",
timeout_ms,
elapsed_ms,
rc,
(unsigned long long)g_poll_counter);
} else if ((g_poll_counter % 200ULL) == 0ULL) {
DEBUG_TRACE("[didactyl] poll heartbeat: timeout=%d elapsed=%.1fms rc=%d count=%llu",
timeout_ms,
elapsed_ms,
rc,
(unsigned long long)g_poll_counter);
}
}
}
log_relay_state_changes();
return rc;
+1
View File
@@ -37,6 +37,7 @@ void nostr_handler_publish_result_free(nostr_publish_result_t* result);
char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
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);
const char* nostr_handler_get_startup_display_name(void);
int nostr_handler_connected_relay_count(void);
+4 -4
View File
@@ -268,7 +268,7 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
cJSON_AddItemToObject(adopt_filter, "authors", authors);
cJSON_AddNumberToObject(adopt_filter, "limit", 1);
char* adoption_json = nostr_handler_query_json(adopt_filter, 8000);
char* adoption_json = nostr_handler_query_json(adopt_filter, 2000);
cJSON_Delete(adopt_filter);
if (!adoption_json) {
return 0;
@@ -328,7 +328,7 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
cJSON_AddItemToObject(skill_filter, "#d", d_values);
cJSON_AddNumberToObject(skill_filter, "limit", 1);
char* skill_json = nostr_handler_query_json(skill_filter, 8000);
char* skill_json = nostr_handler_query_json(skill_filter, 2000);
cJSON_Delete(skill_filter);
if (!skill_json) {
continue;
@@ -513,7 +513,7 @@ int trigger_manager_poll(trigger_manager_t* mgr) {
}
time_t now = time(NULL);
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 2) {
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 10) {
return 0;
}
mgr->last_poll_at = now;
@@ -538,7 +538,7 @@ int trigger_manager_poll(trigger_manager_t* mgr) {
cJSON_AddNumberToObject(filter, "since", (double)since);
cJSON_AddNumberToObject(filter, "limit", 8);
char* events_json = nostr_handler_query_json(filter, 4000);
char* events_json = nostr_handler_query_json(filter, 1200);
cJSON_Delete(filter);
if (!events_json) {
continue;