Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c32683e75 | ||
|
|
645f6ea610 | ||
|
|
b569d41409 | ||
|
|
b3b1ddb2dd |
@@ -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.8
|
||||
## Current Status — v0.2.12
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.2.8 — Clean DM context duplication, suppress oversized websocket frame traces, and return raw content/tags in skill_get
|
||||
> Last release update: v0.2.12 — Fix nostr_core_lib EOSE_FIRST semantics and relay pool subscription cleanup
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
File diff suppressed because one or more lines are too long
224
plans/strict_startup_validation.md
Normal file
224
plans/strict_startup_validation.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Strict Startup Validation — Eliminate Fake Event Seeding
|
||||
|
||||
## Problem
|
||||
|
||||
The agent currently seeds a **synthetic event** into the self-skill cache during startup
|
||||
(`seed_default_skill_into_cache()` in `src/nostr_handler.c:1299`). This fake event has:
|
||||
|
||||
- `id` = all zeros
|
||||
- `created_at` = `time(NULL)` (current startup time)
|
||||
|
||||
Because the fake event is newer than the real relay event, the upsert rejects the real
|
||||
event. This causes `skill_get` to return an all-zeros ID and prevents the cache from
|
||||
ever holding the authentic relay event.
|
||||
|
||||
Additionally, the adopted skills cache in `src/agent.c` has a startup fallback path
|
||||
that uses `g_cfg->startup_events` when the adoption list is empty. This masks real
|
||||
failures and creates inconsistency between what the agent thinks it has and what
|
||||
actually exists on relays.
|
||||
|
||||
## Principle
|
||||
|
||||
> If the agent cannot retrieve the events it needs to run, startup should fail with
|
||||
> a clear diagnostic. No fake events. No fallbacks. If it is not there, fail.
|
||||
|
||||
## Two Startup Modes
|
||||
|
||||
### Genesis Startup (first run)
|
||||
|
||||
1. Connect to relays
|
||||
2. Publish startup events (profile, contacts, relay list, default skill, adoption list)
|
||||
3. **Wait for publish confirmation** from at least one relay per event
|
||||
4. Log at INFO level: which events were published to which relays
|
||||
5. Subscribe to self-skill cache
|
||||
6. Wait for EOSE — validate published events are now in cache
|
||||
7. If validation fails: abort with diagnostic
|
||||
|
||||
### Subsequent Startup (existing agent)
|
||||
|
||||
1. Connect to relays
|
||||
2. Subscribe to self-skill cache
|
||||
3. Wait for EOSE
|
||||
4. Log at INFO level: which events were received from which relays
|
||||
5. Validate required events exist in cache
|
||||
6. If validation fails: abort with diagnostic
|
||||
|
||||
## Current Startup Flow (steps 14–18)
|
||||
|
||||
```
|
||||
Step 14: Subscribe self-skill cache (sends REQ for kinds 31123/31124/10123)
|
||||
→ Does NOT wait for EOSE
|
||||
→ Calls seed_default_skill_into_cache() with fake event
|
||||
Step 15: Subscribe DMs (agent starts accepting messages)
|
||||
→ Startup DM sent BEFORE this step
|
||||
Step 16: Subscribe wallet events
|
||||
Step 17: Initialize cashu wallet
|
||||
Step 18: READY — enter main poll loop
|
||||
```
|
||||
|
||||
## Proposed New Flow
|
||||
|
||||
```
|
||||
Step 14: Subscribe self-skill cache (sends REQ for kinds 31123/31124/10123)
|
||||
Step 14a: Wait for self-skill EOSE (15s timeout)
|
||||
Step 14b: Validate required events exist in cache
|
||||
→ Log received events at INFO level
|
||||
→ If no skill events: FAIL startup with diagnostic
|
||||
Step 15: Subscribe DMs
|
||||
Step 16: Subscribe wallet events
|
||||
Step 17: Initialize cashu wallet
|
||||
Step 18: READY — send startup DM AFTER all validation, enter main loop
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Remove `seed_default_skill_into_cache()` entirely
|
||||
|
||||
**File:** `src/nostr_handler.c`
|
||||
|
||||
- Delete the entire function (lines 1299–1341)
|
||||
- Remove its call at line 3165 in `nostr_handler_reconcile_startup_events()`
|
||||
- Remove its forward declaration at line 451
|
||||
|
||||
### 2. Remove startup fallback in `refresh_adopted_skills_cache_if_needed()`
|
||||
|
||||
**File:** `src/agent.c`
|
||||
|
||||
Delete the entire fallback block (lines ~1571–1619) that uses
|
||||
`g_cfg->startup_events` when the adoption list is empty. If the adoption
|
||||
list is empty, the cache stays empty — no synthetic population.
|
||||
|
||||
### 3. Add synchronous EOSE wait for self-skill subscription
|
||||
|
||||
**File:** `src/nostr_handler.c`
|
||||
|
||||
Add a new exported function:
|
||||
|
||||
```c
|
||||
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
|
||||
```
|
||||
|
||||
Implementation:
|
||||
- Use a static `volatile int g_self_skill_eose_received = 0` flag
|
||||
- Set it to 1 in `on_self_skill_eose()`
|
||||
- The wait function polls `nostr_relay_pool_poll()` in a loop until the
|
||||
flag is set or the timeout expires
|
||||
- Returns 0 on success, -1 on timeout
|
||||
|
||||
### 4. Add post-EOSE validation with logging
|
||||
|
||||
**File:** `src/nostr_handler.c`
|
||||
|
||||
Add a new exported function:
|
||||
|
||||
```c
|
||||
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
|
||||
```
|
||||
|
||||
This function:
|
||||
- Counts skill events (kind 31123 + 31124) in cache
|
||||
- Counts adoption events (kind 10123) in cache
|
||||
- Logs at INFO level each cached event: kind, d_tag, id, created_at
|
||||
- Returns 0 if at least one skill event exists
|
||||
- Returns -1 if no skill events found
|
||||
|
||||
### 5. Update startup sequence in `main.c`
|
||||
|
||||
**File:** `src/main.c`
|
||||
|
||||
After step 14 subscribe, add EOSE wait and validation:
|
||||
|
||||
```c
|
||||
// Wait for self-skill EOSE
|
||||
int eose_timeout_ms = 15000;
|
||||
if (nostr_handler_wait_for_self_skill_eose(eose_timeout_ms) != 0) {
|
||||
startup_step_fail(14, "Subscribe self-skill cache",
|
||||
"self-skill events not received within timeout; "
|
||||
"check relay connectivity and that skill events exist");
|
||||
// cleanup and return 1
|
||||
}
|
||||
|
||||
// Validate
|
||||
int skill_count = 0, adoption_count = 0;
|
||||
if (nostr_handler_validate_self_skill_cache(&skill_count, &adoption_count) != 0) {
|
||||
startup_step_fail(14, "Subscribe self-skill cache",
|
||||
"no skill events found after EOSE; "
|
||||
"run genesis to publish default skill events");
|
||||
// cleanup and return 1
|
||||
}
|
||||
|
||||
char detail[128];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"skills=%d adoptions=%d", skill_count, adoption_count);
|
||||
startup_step_ok(14, "Subscribe self-skill cache", detail);
|
||||
```
|
||||
|
||||
### 6. Move startup DM to after all validation
|
||||
|
||||
**File:** `src/main.c`
|
||||
|
||||
Move the startup DM send from its current position (between steps 14 and 15)
|
||||
to just before step 18 READY. The agent should only announce itself as online
|
||||
after all required events have been validated and all subscriptions are active.
|
||||
|
||||
### 7. Genesis publish confirmation logging
|
||||
|
||||
**File:** `src/nostr_handler.c` / `src/main.c`
|
||||
|
||||
For genesis startup, the existing publish path already logs at INFO level:
|
||||
```
|
||||
kind 31124 event published to wss://relay.damus.io (async)
|
||||
```
|
||||
|
||||
Enhance this to also log a summary after all startup events are published:
|
||||
```
|
||||
[INFO] startup publish summary: 5 events published to 3/4 relays
|
||||
[INFO] kind=0 (profile) → relay.damus.io, relay.primal.net, nos.lol
|
||||
[INFO] kind=31124 (default_admin_dm) → relay.damus.io, relay.primal.net, nos.lol
|
||||
[INFO] kind=10123 (adoption list) → relay.damus.io, relay.primal.net, nos.lol
|
||||
```
|
||||
|
||||
### 8. Declare new functions in header
|
||||
|
||||
**File:** `src/nostr_handler.h`
|
||||
|
||||
```c
|
||||
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
|
||||
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
|
||||
```
|
||||
|
||||
## Startup Step Summary
|
||||
|
||||
| Step | Description | Behavior |
|
||||
|------|-------------|----------|
|
||||
| 1–13 | Existing steps (config, relays, agent init, triggers, kind10002, context subs) | Unchanged |
|
||||
| 14 | Subscribe self-skill cache + wait EOSE + validate | **BLOCKING** — fails if no skill events |
|
||||
| 15 | Subscribe DMs | Fails startup if subscription fails |
|
||||
| 16 | Subscribe wallet events | Continues on failure |
|
||||
| 17 | Initialize cashu wallet | Continues on failure |
|
||||
| 18 | READY | Send startup DM, enter main loop |
|
||||
|
||||
## Error Messages
|
||||
|
||||
### EOSE timeout
|
||||
```
|
||||
[ERROR] Startup aborted: self-skill EOSE not received within 15000ms.
|
||||
[ERROR] Check relay connectivity (connected=2/4) and ensure skill events exist on relays.
|
||||
[ERROR] If this is a first run, use genesis to publish startup events.
|
||||
```
|
||||
|
||||
### No skill events after EOSE
|
||||
```
|
||||
[ERROR] Startup aborted: no skill events (kind 31123/31124) found in self-skill cache.
|
||||
[ERROR] The agent requires at least one skill event to operate.
|
||||
[ERROR] Run genesis to publish the default skill, or publish a skill manually.
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/nostr_handler.c` | Remove `seed_default_skill_into_cache()`, add EOSE wait + validation functions, enhance publish logging |
|
||||
| `src/nostr_handler.h` | Declare `nostr_handler_wait_for_self_skill_eose()` and `nostr_handler_validate_self_skill_cache()` |
|
||||
| `src/main.c` | Add EOSE wait + validation after step 14, move startup DM to before step 18 |
|
||||
| `src/agent.c` | Remove startup fallback block in `refresh_adopted_skills_cache_if_needed()` |
|
||||
60
src/agent.c
60
src/agent.c
@@ -945,7 +945,11 @@ static __attribute__((unused)) void template_emit_hook(const char* section_name,
|
||||
}
|
||||
|
||||
static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
|
||||
if (phase && strcmp(phase, "direct_tool_exec") == 0) {
|
||||
if (!phase) {
|
||||
return;
|
||||
}
|
||||
if (strcmp(phase, "llm_chat_with_tools_messages") != 0 &&
|
||||
strcmp(phase, "llm_trigger_with_tools_messages") != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -970,7 +974,7 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
|
||||
int entry_len = snprintf(NULL,
|
||||
0,
|
||||
"```text\n"
|
||||
"Context Log - not seen by model\n"
|
||||
"Context Log - seen by model\n"
|
||||
"timestamp=%s\n"
|
||||
"phase=%s\n"
|
||||
"sender=%s\n"
|
||||
@@ -998,7 +1002,7 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
|
||||
snprintf(entry,
|
||||
(size_t)entry_len + 1U,
|
||||
"```text\n"
|
||||
"Context Log - not seen by model\n"
|
||||
"Context Log - seen by model\n"
|
||||
"timestamp=%s\n"
|
||||
"phase=%s\n"
|
||||
"sender=%s\n"
|
||||
@@ -1542,56 +1546,6 @@ static int refresh_adopted_skills_cache_if_needed(void) {
|
||||
|
||||
cJSON_Delete(adoption_events);
|
||||
|
||||
if (tmp_count == 0 && g_cfg->startup_event_count > 0 && g_cfg->startup_events) {
|
||||
for (int i = 0; i < g_cfg->startup_event_count && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
|
||||
startup_event_t* se = &g_cfg->startup_events[i];
|
||||
if (!se || (se->kind != 31123 && se->kind != 31124) || !se->content || se->content[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
char d_tag_val[65] = {0};
|
||||
if (se->tags_json) {
|
||||
cJSON* tags = cJSON_Parse(se->tags_json);
|
||||
cJSON* d_tag = tags ? find_tag_value_string_local(tags, "d") : NULL;
|
||||
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && d_tag->valuestring[0] != '\0') {
|
||||
snprintf(d_tag_val, sizeof(d_tag_val), "%s", d_tag->valuestring);
|
||||
}
|
||||
cJSON_Delete(tags);
|
||||
}
|
||||
if (d_tag_val[0] == '\0') {
|
||||
snprintf(d_tag_val, sizeof(d_tag_val), "startup-skill-%d", i + 1);
|
||||
}
|
||||
|
||||
tmp[tmp_count].kind = se->kind;
|
||||
snprintf(tmp[tmp_count].author_pubkey_hex,
|
||||
sizeof(tmp[tmp_count].author_pubkey_hex),
|
||||
"%s",
|
||||
g_cfg->keys.public_key_hex[0] != '\0'
|
||||
? g_cfg->keys.public_key_hex
|
||||
: "unknown");
|
||||
snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", d_tag_val);
|
||||
tmp[tmp_count].content = strdup(se->content);
|
||||
tmp[tmp_count].has_trigger = 0;
|
||||
tmp[tmp_count].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
|
||||
tmp[tmp_count].filter_json[0] = '\0';
|
||||
if (se->tags_json && se->tags_json[0] != '\0') {
|
||||
cJSON* tags = cJSON_Parse(se->tags_json);
|
||||
cJSON* trigger_tag = tags ? find_tag_value_string_local(tags, "trigger") : NULL;
|
||||
cJSON* filter_tag = tags ? find_tag_value_string_local(tags, "filter") : NULL;
|
||||
if (trigger_tag && cJSON_IsString(trigger_tag) && trigger_tag->valuestring && trigger_tag->valuestring[0] != '\0' &&
|
||||
filter_tag && cJSON_IsString(filter_tag) && filter_tag->valuestring && filter_tag->valuestring[0] != '\0') {
|
||||
tmp[tmp_count].has_trigger = 1;
|
||||
tmp[tmp_count].trigger_type = trigger_type_from_string(trigger_tag->valuestring);
|
||||
snprintf(tmp[tmp_count].filter_json, sizeof(tmp[tmp_count].filter_json), "%s", filter_tag->valuestring);
|
||||
}
|
||||
cJSON_Delete(tags);
|
||||
}
|
||||
if (tmp[tmp_count].content) {
|
||||
tmp_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||
clear_adopted_skills_cache_locked();
|
||||
for (int i = 0; i < tmp_count; i++) {
|
||||
|
||||
117
src/main.c
117
src/main.c
@@ -1317,9 +1317,9 @@ int main(int argc, char** argv) {
|
||||
fprintf(stderr, "Startup aborted: could not retrieve valid self kind 10002 within %d ms\n", kind10002_timeout_ms);
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
@@ -1333,9 +1333,9 @@ int main(int argc, char** argv) {
|
||||
startup_step_fail(11, "Discover self relay list via kind 10002", "failed to apply discovered relay list to pool");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
@@ -1374,34 +1374,74 @@ int main(int argc, char** argv) {
|
||||
startup_step_begin(14, "Subscribe self-skill cache");
|
||||
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)");
|
||||
startup_step_fail(14, "Subscribe self-skill cache", "nostr_handler_subscribe_self_skills failed");
|
||||
DEBUG_ERROR("[didactyl] startup phase: subscribe self skill cache failed");
|
||||
fprintf(stderr, "Startup aborted: failed to subscribe self-skill cache\n");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
|
||||
startup_step_ok(14, "Subscribe self-skill cache", NULL);
|
||||
|
||||
char startup_dm[768];
|
||||
const char* startup_name = nostr_handler_get_startup_display_name();
|
||||
int connected_relays = nostr_handler_connected_relay_count();
|
||||
time_t startup_now = time(NULL);
|
||||
struct tm startup_tm;
|
||||
char startup_time_str[32] = {0};
|
||||
if (localtime_r(&startup_now, &startup_tm)) {
|
||||
(void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
|
||||
} else {
|
||||
snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
|
||||
const int self_skill_eose_timeout_ms = 15000;
|
||||
if (nostr_handler_wait_for_self_skill_eose(self_skill_eose_timeout_ms) != 0) {
|
||||
startup_step_fail(14,
|
||||
"Subscribe self-skill cache",
|
||||
"self-skill EOSE not received within timeout");
|
||||
fprintf(stderr,
|
||||
"Startup aborted: self-skill EOSE not received within %d ms\n",
|
||||
self_skill_eose_timeout_ms);
|
||||
fprintf(stderr,
|
||||
"Check relay connectivity (connected=%d/%d) and ensure skill events exist on relays\n",
|
||||
nostr_handler_connected_relay_count(),
|
||||
cfg.relay_count);
|
||||
fprintf(stderr,
|
||||
"If this is a first run, use genesis to publish startup events\n");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
snprintf(startup_dm,
|
||||
sizeof(startup_dm),
|
||||
"%s has started up and is online at %s (version %s, connected relays: %d/%d).",
|
||||
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
|
||||
startup_time_str,
|
||||
DIDACTYL_VERSION,
|
||||
connected_relays,
|
||||
cfg.relay_count);
|
||||
if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
|
||||
|
||||
int startup_skill_count = 0;
|
||||
int startup_adoption_count = 0;
|
||||
if (nostr_handler_validate_self_skill_cache(&startup_skill_count, &startup_adoption_count) != 0) {
|
||||
startup_step_fail(14,
|
||||
"Subscribe self-skill cache",
|
||||
"no skill events found after EOSE");
|
||||
fprintf(stderr,
|
||||
"Startup aborted: no skill events (kind 31123/31124) found in self-skill cache\n");
|
||||
fprintf(stderr,
|
||||
"The agent requires at least one skill event to operate\n");
|
||||
fprintf(stderr,
|
||||
"Run genesis to publish the default skill, or publish a skill manually\n");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
char self_skill_detail[128];
|
||||
snprintf(self_skill_detail,
|
||||
sizeof(self_skill_detail),
|
||||
"skills=%d adoptions=%d",
|
||||
startup_skill_count,
|
||||
startup_adoption_count);
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
|
||||
startup_step_ok(14, "Subscribe self-skill cache", self_skill_detail);
|
||||
|
||||
startup_step_begin(15, "Subscribe DMs");
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
|
||||
if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
|
||||
@@ -1410,9 +1450,9 @@ int main(int argc, char** argv) {
|
||||
fprintf(stderr, "Failed to subscribe to DMs\n");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
@@ -1497,6 +1537,29 @@ int main(int argc, char** argv) {
|
||||
|
||||
nostr_handler_refresh_relay_statuses();
|
||||
|
||||
char startup_dm[768];
|
||||
const char* startup_name = nostr_handler_get_startup_display_name();
|
||||
int connected_relays = nostr_handler_connected_relay_count();
|
||||
time_t startup_now = time(NULL);
|
||||
struct tm startup_tm;
|
||||
char startup_time_str[32] = {0};
|
||||
if (localtime_r(&startup_now, &startup_tm)) {
|
||||
(void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
|
||||
} else {
|
||||
snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
|
||||
}
|
||||
snprintf(startup_dm,
|
||||
sizeof(startup_dm),
|
||||
"%s has started up and is online at %s (version %s, connected relays: %d/%d).",
|
||||
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
|
||||
startup_time_str,
|
||||
DIDACTYL_VERSION,
|
||||
connected_relays,
|
||||
cfg.relay_count);
|
||||
if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
|
||||
}
|
||||
|
||||
startup_step_ok(18, "READY", "agent online; entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
|
||||
@@ -1520,9 +1583,9 @@ int main(int argc, char** argv) {
|
||||
cashu_wallet_cleanup();
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
|
||||
|
||||
@@ -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 8
|
||||
#define DIDACTYL_VERSION "v0.2.8"
|
||||
#define DIDACTYL_VERSION_PATCH 12
|
||||
#define DIDACTYL_VERSION "v0.2.12"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -96,6 +96,7 @@ static external_subscription_wrapper_t* g_external_sub_wrappers = NULL;
|
||||
|
||||
static cJSON* g_self_skill_events = NULL;
|
||||
static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static volatile int g_self_skill_eose_received = 0;
|
||||
|
||||
#define DM_DEDUP_CACHE_SIZE 256
|
||||
|
||||
@@ -448,7 +449,6 @@ static int managed_subscription_register_locked(managed_subscription_id_t id,
|
||||
static int managed_subscriptions_resubscribe_all_locked(void);
|
||||
static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id);
|
||||
static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label);
|
||||
static void seed_default_skill_into_cache(void);
|
||||
|
||||
static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url) {
|
||||
if (!relays || relay_count <= 0 || !relay_url || relay_url[0] == '\0') {
|
||||
@@ -1296,50 +1296,6 @@ static void self_skill_cache_upsert_event_locked(cJSON* event) {
|
||||
cJSON_AddItemToArray(g_self_skill_events, dup);
|
||||
}
|
||||
|
||||
static void seed_default_skill_into_cache(void) {
|
||||
if (!g_cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_cfg->default_skill.kind != 31123 && g_cfg->default_skill.kind != 31124) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_cfg->keys.public_key_hex[0] == '\0' ||
|
||||
!g_cfg->default_skill.content || g_cfg->default_skill.content[0] == '\0' ||
|
||||
!g_cfg->default_skill.tags_json || g_cfg->default_skill.tags_json[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_Parse(g_cfg->default_skill.tags_json);
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(tags);
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* event = cJSON_CreateObject();
|
||||
if (!event) {
|
||||
cJSON_Delete(tags);
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = (double)time(NULL);
|
||||
cJSON_AddNumberToObject(event, "kind", g_cfg->default_skill.kind);
|
||||
cJSON_AddStringToObject(event, "pubkey", g_cfg->keys.public_key_hex);
|
||||
cJSON_AddStringToObject(event, "content", g_cfg->default_skill.content);
|
||||
cJSON_AddItemToObject(event, "tags", tags);
|
||||
cJSON_AddNumberToObject(event, "created_at", now);
|
||||
cJSON_AddStringToObject(event,
|
||||
"id",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000");
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
self_skill_cache_upsert_event_locked(event);
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
|
||||
cJSON_Delete(event);
|
||||
}
|
||||
|
||||
static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
|
||||
if (!hex || !out_pubkey || strlen(hex) != 64U) {
|
||||
return -1;
|
||||
@@ -1753,6 +1709,7 @@ static void on_eose(cJSON** events, int event_count, void* user_data) {
|
||||
static void on_self_skill_eose(cJSON** events, int event_count, void* user_data) {
|
||||
(void)events;
|
||||
(void)user_data;
|
||||
g_self_skill_eose_received = 1;
|
||||
DEBUG_INFO("[didactyl] self-skill EOSE received: cached skill events=%d", event_count);
|
||||
|
||||
if (g_self_skill_eose_cb) {
|
||||
@@ -2162,7 +2119,6 @@ static void on_agent_context_event(cJSON* event, const char* relay_url, void* us
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -2173,6 +2129,20 @@ static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
|
||||
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
||||
cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||||
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
|
||||
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1;
|
||||
const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL;
|
||||
|
||||
DEBUG_INFO("[didactyl] self-skill cache received kind=%d d_tag=%s id=%s created_at=%lld via %s",
|
||||
kind_val,
|
||||
(d_tag && d_tag[0] != '\0') ? d_tag : "-",
|
||||
(id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "<no-id>",
|
||||
(long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0),
|
||||
relay_url ? relay_url : "unknown relay");
|
||||
|
||||
register_trigger_from_self_skill_event(event);
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
@@ -2470,6 +2440,7 @@ int nostr_handler_subscribe_self_skills(void) {
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
g_self_skill_eose_received = 0;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
@@ -2498,9 +2469,9 @@ int nostr_handler_subscribe_self_skills(void) {
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
NOSTR_POOL_EOSE_FULL_SET,
|
||||
30,
|
||||
120,
|
||||
NOSTR_POOL_EOSE_FIRST,
|
||||
12,
|
||||
14,
|
||||
1) != 0) {
|
||||
rc = -1;
|
||||
}
|
||||
@@ -2719,6 +2690,77 @@ void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callb
|
||||
g_self_skill_eose_user_data = user_data;
|
||||
}
|
||||
|
||||
int nostr_handler_wait_for_self_skill_eose(int timeout_ms) {
|
||||
if (!g_pool || timeout_ms <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct timespec ts_start;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts_start) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (!g_self_skill_eose_received) {
|
||||
(void)nostr_handler_poll(100);
|
||||
|
||||
struct timespec ts_now;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts_now) != 0) {
|
||||
return -1;
|
||||
}
|
||||
long long elapsed_ms = (long long)(ts_now.tv_sec - ts_start.tv_sec) * 1000LL +
|
||||
(long long)(ts_now.tv_nsec - ts_start.tv_nsec) / 1000000LL;
|
||||
if (elapsed_ms >= (long long)timeout_ms) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count) {
|
||||
int skill_count = 0;
|
||||
int adoption_count = 0;
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
|
||||
int n = cJSON_GetArraySize(g_self_skill_events);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
|
||||
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
|
||||
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
|
||||
cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL;
|
||||
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
|
||||
if (!kind || !cJSON_IsNumber(kind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int k = (int)kind->valuedouble;
|
||||
if (k == 31123 || k == 31124) {
|
||||
skill_count++;
|
||||
} else if (k == 10123) {
|
||||
adoption_count++;
|
||||
}
|
||||
|
||||
const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL;
|
||||
DEBUG_INFO("[didactyl] self-skill cache event: kind=%d d_tag=%s id=%s created_at=%lld",
|
||||
k,
|
||||
(d_tag && d_tag[0] != '\0') ? d_tag : "-",
|
||||
(id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "<no-id>",
|
||||
(long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0));
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
|
||||
if (out_skill_count) {
|
||||
*out_skill_count = skill_count;
|
||||
}
|
||||
if (out_adoption_count) {
|
||||
*out_adoption_count = adoption_count;
|
||||
}
|
||||
|
||||
return skill_count > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) {
|
||||
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) {
|
||||
return -1;
|
||||
@@ -3134,6 +3176,19 @@ static void publish_pending_startup_events_for_relay_index(int relay_index, cons
|
||||
}
|
||||
}
|
||||
|
||||
static const char* startup_kind_label(int kind) {
|
||||
switch (kind) {
|
||||
case 0: return "profile";
|
||||
case 1: return "note";
|
||||
case 3: return "contacts";
|
||||
case 10002: return "relay_list";
|
||||
case 10123: return "adoption_list";
|
||||
case 31123: return "private_skill";
|
||||
case 31124: return "skill";
|
||||
default: return "event";
|
||||
}
|
||||
}
|
||||
|
||||
int nostr_handler_reconcile_startup_events(void) {
|
||||
if (!g_cfg || !g_pool) {
|
||||
return -1;
|
||||
@@ -3162,7 +3217,54 @@ int nostr_handler_reconcile_startup_events(void) {
|
||||
publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile");
|
||||
}
|
||||
|
||||
seed_default_skill_into_cache();
|
||||
if (g_startup_publish_tracking_enabled && g_startup_published) {
|
||||
int events_published = 0;
|
||||
int relays_used = 0;
|
||||
|
||||
for (int r = 0; r < g_cfg->relay_count; r++) {
|
||||
int used = 0;
|
||||
for (int i = 0; i < g_cfg->startup_event_count; i++) {
|
||||
size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r;
|
||||
if (g_startup_published[slot]) {
|
||||
used = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (used) {
|
||||
relays_used++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_cfg->startup_event_count; i++) {
|
||||
startup_event_t* se = &g_cfg->startup_events[i];
|
||||
char relays_line[512];
|
||||
relays_line[0] = '\0';
|
||||
int relay_hits = 0;
|
||||
for (int r = 0; r < g_cfg->relay_count; r++) {
|
||||
size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r;
|
||||
if (!g_startup_published[slot]) {
|
||||
continue;
|
||||
}
|
||||
relay_hits++;
|
||||
if (relays_line[0] != '\0') {
|
||||
strncat(relays_line, ", ", sizeof(relays_line) - strlen(relays_line) - 1U);
|
||||
}
|
||||
strncat(relays_line, g_cfg->relays[r], sizeof(relays_line) - strlen(relays_line) - 1U);
|
||||
}
|
||||
if (relay_hits > 0) {
|
||||
events_published++;
|
||||
DEBUG_INFO("[didactyl] startup publish detail: kind=%d (%s) -> %s",
|
||||
se->kind,
|
||||
startup_kind_label(se->kind),
|
||||
relays_line[0] ? relays_line : "<none>");
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] startup publish summary: %d event(s) published to %d/%d relay(s)",
|
||||
events_published,
|
||||
relays_used,
|
||||
g_cfg->relay_count);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
|
||||
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription);
|
||||
int nostr_handler_poll(int timeout_ms);
|
||||
int nostr_handler_reconcile_startup_events(void);
|
||||
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
|
||||
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
|
||||
void nostr_handler_refresh_relay_statuses(void);
|
||||
int nostr_handler_sync_relays_from_config(void);
|
||||
char* nostr_handler_get_self_skill_events_json(void);
|
||||
|
||||
Reference in New Issue
Block a user