# 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()` |
