From 0fc8af96e41a2e4fe0e0ab69e6bbb7ef563ae8cf Mon Sep 17 00:00:00 2001 From: Didactyl User Date: Thu, 9 Apr 2026 07:42:08 -0400 Subject: [PATCH] v0.2.32 - Made user-settings recall resilient and backfill missing didactyl admin_pubkey/dm_protocol on republish --- README.md | 4 +-- src/main.c | 65 ++++++++++++++++++++++++++++++++++------------ src/main.h | 4 +-- src/setup_wizard.c | 33 +++++++++++++---------- 4 files changed, 72 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 3f9e9f2..dd37168 100644 --- a/README.md +++ b/README.md @@ -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.31 +## Current Status — v0.2.32 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.31 — Added kind 10050 to genesis.jsonc +> Last release update: v0.2.32 — Made user-settings recall resilient and backfill missing didactyl admin_pubkey/dm_protocol on republish - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/src/main.c b/src/main.c index 4ea7054..65010f1 100644 --- a/src/main.c +++ b/src/main.c @@ -741,8 +741,13 @@ static int fetch_self_config_plaintext(const didactyl_config_t* cfg, const char* return 0; } -static int apply_recalled_user_settings(didactyl_config_t* cfg, const char* plaintext) { +static int apply_recalled_user_settings(didactyl_config_t* cfg, + const char* plaintext, + int* out_backfill_needed) { if (!cfg || !plaintext) return -1; + if (out_backfill_needed) { + *out_backfill_needed = 0; + } cJSON* root = cJSON_Parse(plaintext); if (!root || !cJSON_IsObject(root)) { @@ -752,7 +757,7 @@ static int apply_recalled_user_settings(didactyl_config_t* cfg, const char* plai cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm"); cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(root, "didactyl"); - if (!global_llm || !cJSON_IsObject(global_llm) || !didactyl || !cJSON_IsObject(didactyl)) { + if (!global_llm || !cJSON_IsObject(global_llm)) { cJSON_Delete(root); return -1; } @@ -764,15 +769,20 @@ static int apply_recalled_user_settings(didactyl_config_t* cfg, const char* plai cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(global_llm, "max_tokens"); cJSON* temperature = cJSON_GetObjectItemCaseSensitive(global_llm, "temperature"); - cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey"); - cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol"); - cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns"); + cJSON* admin_pubkey = NULL; + cJSON* dm_protocol = NULL; + cJSON* max_turns = NULL; + if (didactyl && cJSON_IsObject(didactyl)) { + admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey"); + dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol"); + max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns"); + } else if (out_backfill_needed) { + *out_backfill_needed = 1; + } if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' || !model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' || - !base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0' || - !admin_pubkey || !cJSON_IsString(admin_pubkey) || !admin_pubkey->valuestring || admin_pubkey->valuestring[0] == '\0' || - !dm_protocol || !cJSON_IsString(dm_protocol) || !dm_protocol->valuestring || dm_protocol->valuestring[0] == '\0') { + !base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') { cJSON_Delete(root); return -1; } @@ -793,16 +803,22 @@ static int apply_recalled_user_settings(didactyl_config_t* cfg, const char* plai cfg->llm.temperature = temperature->valuedouble; } - { + if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') { char decoded[65] = {0}; if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) != 0) { cJSON_Delete(root); return -1; } snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded); + } else if (out_backfill_needed) { + *out_backfill_needed = 1; } - dm_protocol_from_string(dm_protocol->valuestring, cfg); + if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') { + dm_protocol_from_string(dm_protocol->valuestring, cfg); + } else if (out_backfill_needed) { + *out_backfill_needed = 1; + } if (max_turns && cJSON_IsNumber(max_turns)) { int parsed = (int)max_turns->valuedouble; @@ -903,10 +919,13 @@ static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg, int per return rc; } -static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) { +static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg, int* out_backfill_needed) { if (!cfg) { return 0; } + if (out_backfill_needed) { + *out_backfill_needed = 0; + } char* settings_plaintext = NULL; if (fetch_self_config_plaintext(cfg, "user-settings", &settings_plaintext) != 0 || !settings_plaintext) { @@ -915,14 +934,22 @@ static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) { return 0; } - if (apply_recalled_user_settings(cfg, settings_plaintext) != 0) { + int backfill_needed = 0; + if (apply_recalled_user_settings(cfg, settings_plaintext, &backfill_needed) != 0) { DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled user-settings"); free(settings_plaintext); return 0; } free(settings_plaintext); - DEBUG_INFO("[didactyl] startup phase: recovered user-settings from Nostr"); + if (out_backfill_needed) { + *out_backfill_needed = backfill_needed; + } + if (backfill_needed) { + DEBUG_INFO("[didactyl] startup phase: recovered user-settings from Nostr (missing didactyl fields will be backfilled)"); + } else { + DEBUG_INFO("[didactyl] startup phase: recovered user-settings from Nostr"); + } return 1; } @@ -1203,8 +1230,13 @@ int main(int argc, char** argv) { } startup_step_begin(2, "Recover runtime config from Nostr"); - int llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg); - startup_step_ok(2, "Recover runtime config from Nostr", llm_recalled_from_nostr ? "user-settings recalled" : NULL); + int user_settings_backfill_needed = 0; + int llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg, &user_settings_backfill_needed); + startup_step_ok(2, + "Recover runtime config from Nostr", + llm_recalled_from_nostr + ? (user_settings_backfill_needed ? "user-settings recalled (backfill required)" : "user-settings recalled") + : NULL); startup_step_begin(3, "Validate LLM config"); if (!llm_config_is_complete(&cfg.llm)) { @@ -1266,7 +1298,8 @@ int main(int argc, char** argv) { } if (bootstrap_mode || first_run) { - if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) { + if (persist_runtime_config_to_nostr(&cfg, + (llm_recalled_from_nostr && !user_settings_backfill_needed) ? 0 : 1) != 0) { DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr"); } else { DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr"); diff --git a/src/main.h b/src/main.h index 933af73..df0bae8 100644 --- a/src/main.h +++ b/src/main.h @@ -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 31 -#define DIDACTYL_VERSION "v0.2.31" +#define DIDACTYL_VERSION_PATCH 32 +#define DIDACTYL_VERSION "v0.2.32" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/setup_wizard.c b/src/setup_wizard.c index 846661a..86003c5 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -1019,7 +1019,7 @@ static int apply_recalled_user_settings_wizard(didactyl_config_t* cfg, const cha cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm"); cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(root, "didactyl"); - if (!global_llm || !cJSON_IsObject(global_llm) || !didactyl || !cJSON_IsObject(didactyl)) { + if (!global_llm || !cJSON_IsObject(global_llm)) { cJSON_Delete(root); return -1; } @@ -1031,15 +1031,18 @@ static int apply_recalled_user_settings_wizard(didactyl_config_t* cfg, const cha cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(global_llm, "max_tokens"); cJSON* temperature = cJSON_GetObjectItemCaseSensitive(global_llm, "temperature"); - cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey"); - cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol"); - cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns"); + cJSON* admin_pubkey = NULL; + cJSON* dm_protocol = NULL; + cJSON* max_turns = NULL; + if (didactyl && cJSON_IsObject(didactyl)) { + admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey"); + dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol"); + max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns"); + } if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' || !model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' || - !base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0' || - !admin_pubkey || !cJSON_IsString(admin_pubkey) || !admin_pubkey->valuestring || admin_pubkey->valuestring[0] == '\0' || - !dm_protocol || !cJSON_IsString(dm_protocol) || !dm_protocol->valuestring || dm_protocol->valuestring[0] == '\0') { + !base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') { cJSON_Delete(root); return -1; } @@ -1060,7 +1063,7 @@ static int apply_recalled_user_settings_wizard(didactyl_config_t* cfg, const cha cfg->llm.temperature = temperature->valuedouble; } - { + if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') { char decoded[65] = {0}; if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) != 0) { cJSON_Delete(root); @@ -1069,12 +1072,14 @@ static int apply_recalled_user_settings_wizard(didactyl_config_t* cfg, const cha snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded); } - if (strcmp(dm_protocol->valuestring, "nip17") == 0) { - cfg->dm_protocol = DM_PROTOCOL_NIP17; - } else if (strcmp(dm_protocol->valuestring, "both") == 0) { - cfg->dm_protocol = DM_PROTOCOL_BOTH; - } else { - cfg->dm_protocol = DM_PROTOCOL_NIP04; + if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') { + if (strcmp(dm_protocol->valuestring, "nip17") == 0) { + cfg->dm_protocol = DM_PROTOCOL_NIP17; + } else if (strcmp(dm_protocol->valuestring, "both") == 0) { + cfg->dm_protocol = DM_PROTOCOL_BOTH; + } else { + cfg->dm_protocol = DM_PROTOCOL_NIP04; + } } if (max_turns && cJSON_IsNumber(max_turns)) {