5.4 KiB
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_listreturnscount: 1with onlyinfrastructure-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_relaysand 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:
- Checks if
g_cfg->default_skill.contentis non-empty - Builds a cJSON event object with:
kind:g_cfg->default_skill.kind(31124)pubkey:g_cfg->keys.public_key_hexcontent:g_cfg->default_skill.contenttags: parsed fromg_cfg->default_skill.tags_jsoncreated_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
- 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
src/nostr_handler.c:- Add
seed_default_skill_into_cache()static function - Call it at the end of
nostr_handler_reconcile_startup_events()
- Add
Edge Cases
- If the default skill is later updated via
skill_update, the relay version will have a newercreated_atand 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_cachechecks by event ID won't match it — butskill_listiterates all events and doesn't check by ID