7.7 KiB
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 zeroscreated_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)
- Connect to relays
- Publish startup events (profile, contacts, relay list, default skill, adoption list)
- Wait for publish confirmation from at least one relay per event
- Log at INFO level: which events were published to which relays
- Subscribe to self-skill cache
- Wait for EOSE — validate published events are now in cache
- If validation fails: abort with diagnostic
Subsequent Startup (existing agent)
- Connect to relays
- Subscribe to self-skill cache
- Wait for EOSE
- Log at INFO level: which events were received from which relays
- Validate required events exist in cache
- 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:
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
Implementation:
- Use a static
volatile int g_self_skill_eose_received = 0flag - 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:
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:
// 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
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() |