# Fix: Default Skill Missing from skill_list Cache ## Problem When a new didactyl agent is created via the setup wizard, the default skill (`didactyl-default`, kind 31124) does not appear in the `skill_list` output. The agent IS using the default skill as its system context, but the skill cache (`g_self_skill_events`) does not contain it. ## Evidence - Default skill event IS published to relays (confirmed by `nostr_my_events`) - Default skill event shows `in_current_cache: false` - `skill_list` returns `count: 1` with only `infrastructure-monitor` - The kind 10123 adoption list only contains `infrastructure-monitor` ## Root Cause Analysis The startup sequence in `main.c` is: ``` 1. nostr_handler_init() — resets g_self_skill_events to [] 2. wait_for_connected_relays() — waits for relay connections 3. reconcile_startup_events() — publishes default skill to relays -> publish_kind_event_to_relays() — inserts into cache IF sent > 0 4. ... relay list expansion ... 5. subscribe_self_skills() — subscribes to relays for kinds 31123/31124/10123 ``` The default skill SHOULD be inserted into the cache at step 3 via `publish_kind_event_to_relays` -> `self_skill_cache_upsert_event_locked`. However, the cache insert at line 2750 is gated by `if (sent > 0)`. There are two failure modes: ### Failure Mode 1: Async publish timing The startup publish creates a NEW event per relay via `publish_pending_startup_events_for_relay_index`. Each call to `publish_kind_event_to_relays` creates a fresh event with `nostr_create_and_sign_event`. If the relay is not connected at that moment, `sent = 0` and the cache insert is skipped. ### Failure Mode 2: Subscription race condition Even if the cache insert succeeds at step 3, the self-skill subscription at step 5 queries relays. For a brand new agent, the default skill was JUST published asynchronously. If the relay hasn't indexed it by the time the subscription query runs, the subscription returns EOSE without the default skill. The subscription stays open but the default skill was published BEFORE the subscription started, so it won't arrive as a live event. The cache entry from step 3 should persist, but there may be an edge case where the relay pool reconnection handler at line 792 triggers another publish cycle, creating a new event that replaces the cache entry, and if that publish fails (sent=0), the replacement doesn't happen but the old entry is still there. ### Most Likely Cause The most likely cause is that `publish_kind_event_to_relays` returns `sent = 0` for the default skill during the initial reconcile. This can happen if: - The relay connection drops momentarily between `wait_for_connected_relays` and the actual publish - The relay pool's async publish fails silently ## Proposed Fix ### Approach: Seed the cache directly from config during reconcile In `nostr_handler_reconcile_startup_events()`, after publishing startup events, explicitly seed the self-skill cache with a synthetic event built from `g_cfg->default_skill`. This ensures the default skill is ALWAYS in the cache regardless of relay publish success. ### Implementation In `nostr_handler.c`, add a new static function `seed_default_skill_into_cache()` that: 1. Checks if `g_cfg->default_skill.content` is non-empty 2. Builds a cJSON event object with: - `kind`: `g_cfg->default_skill.kind` (31124) - `pubkey`: `g_cfg->keys.public_key_hex` - `content`: `g_cfg->default_skill.content` - `tags`: parsed from `g_cfg->default_skill.tags_json` - `created_at`: `time(NULL)` (or 0 as a floor value) - `id`: a placeholder hex string (e.g., all zeros) — will be replaced when the real event arrives from relay 3. Calls `self_skill_cache_upsert_event_locked()` with this synthetic event Call this function at the END of `nostr_handler_reconcile_startup_events()`, AFTER the publish loop. This way: - If the publish succeeded, the cache already has the real event (with real ID/sig). The synthetic event would have `created_at` <= the real one, so the upsert would be a no-op. - If the publish failed, the cache gets the synthetic event as a fallback. - When the subscription later returns the real event from relays, it replaces the synthetic one (since it has a real ID and potentially newer timestamp). ### Alternative Approach: Remove the `sent > 0` gate Change `publish_kind_event_to_relays` to always insert skill events (kinds 31123/31124) into the cache, regardless of whether the relay publish succeeded. This is simpler but changes the semantics — the cache would contain events that may not be on any relay. ### Recommended: Approach 1 (seed from config) This is safer because: - It doesn't change the publish function's behavior - It explicitly handles the default skill case - The synthetic event gets replaced by the real one when it arrives from relays - It works even if the publish completely fails ### Files to Modify 1. `src/nostr_handler.c`: - Add `seed_default_skill_into_cache()` static function - Call it at the end of `nostr_handler_reconcile_startup_events()` ### Edge Cases - If the default skill is later updated via `skill_update`, the relay version will have a newer `created_at` and will replace the synthetic entry - If the agent has no default skill configured, the seed function is a no-op - The synthetic event has a placeholder ID, so `in_current_cache` checks by event ID won't match it — but `skill_list` iterates all events and doesn't check by ID