Files
didactyl/plans/default_skill_missing_from_cache.md

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_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.

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