From b3b1ddb2dde34bde38f25b61e8766e2bb633bc3e Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 22 Mar 2026 20:01:46 -0400 Subject: [PATCH] v0.2.9 - strict startup validation: remove fake seeding, enforce self-skill EOSE/cache validation --- README.md | 4 +- context.log.md | 66 ++++++++- plans/strict_startup_validation.md | 224 +++++++++++++++++++++++++++++ src/agent.c | 60 +------- src/main.c | 109 +++++++++++--- src/main.h | 4 +- src/nostr_handler.c | 196 +++++++++++++++++++------ src/nostr_handler.h | 2 + 8 files changed, 533 insertions(+), 132 deletions(-) create mode 100644 plans/strict_startup_validation.md diff --git a/README.md b/README.md index 37b39d2..9d69098 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.2.8 +## Current Status — v0.2.9 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.8 — Clean DM context duplication, suppress oversized websocket frame traces, and return raw content/tags in skill_get +> Last release update: v0.2.9 — strict startup validation: remove fake seeding, enforce self-skill EOSE/cache validation - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/context.log.md b/context.log.md index 7f1262b..a4a6299 100644 --- a/context.log.md +++ b/context.log.md @@ -1,14 +1,70 @@ ```text -Context Log - not seen by model -timestamp=2026-03-22 17:56:17 +Context Log - seen by model +timestamp=2026-03-22 19:38:41 phase=llm_chat_with_tools_messages sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 model=claude-opus-4.6 -context_bytes=1318 -approx_tokens=329 +context_bytes=15036 +approx_tokens=3759 ``` -[{"role":"system","content":"You are {\n \"name\": \"Didactyl Agent\",\n \"display_name\": \"Didactyl Agent\",\n \"about\": \"A sovereign AI agent on Nostr.\\n\\nMission: Help maintain and answer questions about the Didactyl project.\\n\\nhttps://git.laantungir.net/laantungir/didactyl\\n\\nNot your keys, not your Bitcoin.\\nNot your keys, not your Agent.\\nNot your keys, not your Robot.\",\n \"picture\": \"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\n \"banner\": \"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"\n}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone\n\nIncoming DM message:\nhello"},{"role":"user","content":"hello","_ts":1774216577}] +[{"role":"system","content":"You are {\"name\":\"Didactyl Agent\",\"display_name\":\"Didactyl\",\"about\":\"A sovereign AI agent on Nostr\",\"picture\":\"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\"banner\":\"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324},{\"role\":\"assistant\",\"content\":\"Hello! 👋 How can I help you today? I'm the Didactyl Agent — I can answer questions about the Didactyl project, help with Nostr-related tasks, or assist with anything else you need. Just let me know!\",\"created_at\":1774222330},{\"role\":\"user\",\"content\":\"/help\",\"created_at\":1774222358},{\"role\":\"assistant\",\"content\":\"AVAILABLE TOOLS\\n(run / -h for full JSON args template)\\n- admin_identity — Build admin identity context block from cached runtime metadata\\n- agent_identity — Build agent identity context block with pubkey and npub\\n- agent_version — Return current Didactyl version and metadata from build macros\\n- blossom_delete — Delete a blob from Blossom\\n- blossom_download — Download a blob from Blossom to a local file\\n- blossom_head — Fetch blob metadata from Blossom\\n- blossom_list — List blobs for a pubkey on Blossom\\n- blossom_upload — Upload a local file to a Blossom server\\n- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit\\n- cashu_wallet_check_proofs — Check current wallet proofs state against mint\\n- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint\\n- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id\\n- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request\\n- cashu_wallet_mint_check — Check a previously requested mint quote status\\n- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state\\n- cashu_wallet_mint_quote — Request a mint quote for a target amount\\n- cashu_wallet_mints_get — Get currently configured wallet and public mints\\n- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both\\n- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state\\n- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount\\n- config_recall — Fetch and decrypt agent config kind 30078 by d_tag\\n- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag\\n- local_file_read — Read a local file as text from the configured working directory\\n- local_file_write — Write text content to a local file in the configured working directory\\n- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body\\n- local_shell_exec — Execute a shell command and return stdout/stderr\\n- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)\\n- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed\\n- model_get — Get current active LLM runtime configuration (excluding API key)\\n- model_list — List available model IDs using provider OpenAI-compatible /models endpoint\\n- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)\\n- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context\\n- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context\\n- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context\\n- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32\\n- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format\\n- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context\\n- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list\\n- nostr_admin_notes — Build admin notes context block from cached kind 1 notes\\n- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata\\n- nostr_admin_relays — Build admin relay context block from cached kind 10002 data\\n- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list\\n- nostr_agent_notes — Build agent notes context block from cached kind 1 notes\\n- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata\\n- nostr_agent_relays — Build agent relay context block from cached kind 10002 data\\n- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default\\n- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache\\n- nostr_decode — Decode a Nostr bech32/nostr: URI into components\\n- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender\\n- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)\\n- nostr_dm_history — Fetch recent DM history for template/context usage; JSON: {\\\"limit\\\":12,\\\"include_current\\\":false} or with explicit peer {\\\"peer_pubkey\\\":\\\"\\\",\\\"limit\\\":20}; template usage: {{nostr_dm_history({\\\"limit\\\":12})}}\\n- nostr_dm_send — Send a NIP-04 encrypted DM\\n- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol\\n- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)\\n- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient\\n- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename\\n- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)\\n- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence\\n- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)\\n- nostr_npub — Return this agent's pubkey encoded as npub bech32\\n- nostr_post — Publish a Nostr event to connected relays\\n- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md\\n- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey\\n- nostr_pubkey — Return this agent's pubkey in hex format\\n- nostr_query — Query events from relays using a Nostr filter\\n- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)\\n- nostr_relay_info — Fetch NIP-11 relay information document\\n- nostr_relay_status — Get connection status and statistics for all relays\\n- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)\\n- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters\\n- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list\\n- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it\\n- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124\\n- skill_get — Get full skill JSON by d tag from local cache; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"} (slash shorthand also works: /skill_get default_admin_dm)\\n- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters\\n- skill_remove — Remove a skill address from kind 10123 adoption list\\n- skill_search — Search public skills by query/author and optionally rank by adoption popularity\\n- skill_set — Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\\\"skill\\\":{\\\"d\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"content\\\":\\\"...\\\",\\\"tags\\\":[[\\\"d\\\",\\\"default_admin_dm\\\"],[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"]]}}\\n- skill_view — Compatibility alias for skill_get; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"}\\n- task_list — Build current task list context block from agent task memory on Nostr\\n- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace\\n- tool_list — List available tools with name, description, and JSON parameter schema\\n- trigger_list — List active triggered skills and their runtime status\\n\\nAVAILABLE SKILLS\\n- default_admin_dm — Default admin DM handler\\n\\nADOPTED SKILLS\\n- default_admin_dm\\n\",\"created_at\":1774222361},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222404},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222404},{\"role\":\"user\",\"content\":\"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\\n\\n /skill_get default_admin_dm\\n7922 3/22/2026, 7:33:24 PM\\nkind4-nip04\\n⋯\\n\\n{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\"from\\\":\\\"admin\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222576},{\"role\":\"assistant\",\"content\":\"Good catch! The relay query found **two versions** of the `default_admin_dm` skill:\\n\\n### Version 1 (older, more complete)\\n- **ID:** `b874122d049aaade3b802db9e80119db0b5c337adb85bd8ba65464d99e44c9a1`\\n- **Created:** `1774221663` (7:21:03 PM)\\n- Has `action`, `enabled` tags ✅\\n- Content has `{{nostr_dm_history}}` on its own line ✅\\n\\n### Version 2 (newer, the one in cache)\\n- **ID:** `bf7f93366ef3a70f30fa64c405631325f58c7f2a15d9217d6b14558ca2189ef7`\\n- **Created:** `1774222308` (7:31:48 PM)\\n- **Missing** `action` and `enabled` tags ⚠️\\n- `{{nostr_dm_history}}` is appended inline to the last rule line ⚠️\\n\\nSo the **real event ID** on relays is `bf7f93...189ef7`, not all zeros. The `skill_get` command returned `0000...0000` as the ID, which appears to be a display bug in the local cache — it has the correct content but didn't store/return the actual event ID.\\n\\nThe newer version (v2) also looks like it was republished with slightly degraded formatting (missing `action`/`enabled` tags and the template got concatenated). Would you like me to republish the skill with the correct tags restored from v1?\",\"created_at\":1774222598},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222631},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222631},{\"role\":\"user\",\"content\":\"What is the capital of Finland?\",\"created_at\":1774222721}]"},{"role":"user","content":"What is the capital of Finland?","_ts":1774222721}] + +--- + +```text +Context Log - seen by model +timestamp=2026-03-22 19:36:28 +phase=llm_chat_with_tools_messages +sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 +model=claude-opus-4.6 +context_bytes=16892 +approx_tokens=4223 +``` + +[{"role":"system","content":"You are {\"name\":\"Didactyl Agent\",\"display_name\":\"Didactyl\",\"about\":\"A sovereign AI agent on Nostr\",\"picture\":\"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\"banner\":\"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324},{\"role\":\"assistant\",\"content\":\"Hello! 👋 How can I help you today? I'm the Didactyl Agent — I can answer questions about the Didactyl project, help with Nostr-related tasks, or assist with anything else you need. Just let me know!\",\"created_at\":1774222330},{\"role\":\"user\",\"content\":\"/help\",\"created_at\":1774222358},{\"role\":\"assistant\",\"content\":\"AVAILABLE TOOLS\\n(run / -h for full JSON args template)\\n- admin_identity — Build admin identity context block from cached runtime metadata\\n- agent_identity — Build agent identity context block with pubkey and npub\\n- agent_version — Return current Didactyl version and metadata from build macros\\n- blossom_delete — Delete a blob from Blossom\\n- blossom_download — Download a blob from Blossom to a local file\\n- blossom_head — Fetch blob metadata from Blossom\\n- blossom_list — List blobs for a pubkey on Blossom\\n- blossom_upload — Upload a local file to a Blossom server\\n- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit\\n- cashu_wallet_check_proofs — Check current wallet proofs state against mint\\n- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint\\n- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id\\n- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request\\n- cashu_wallet_mint_check — Check a previously requested mint quote status\\n- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state\\n- cashu_wallet_mint_quote — Request a mint quote for a target amount\\n- cashu_wallet_mints_get — Get currently configured wallet and public mints\\n- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both\\n- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state\\n- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount\\n- config_recall — Fetch and decrypt agent config kind 30078 by d_tag\\n- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag\\n- local_file_read — Read a local file as text from the configured working directory\\n- local_file_write — Write text content to a local file in the configured working directory\\n- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body\\n- local_shell_exec — Execute a shell command and return stdout/stderr\\n- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)\\n- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed\\n- model_get — Get current active LLM runtime configuration (excluding API key)\\n- model_list — List available model IDs using provider OpenAI-compatible /models endpoint\\n- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)\\n- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context\\n- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context\\n- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context\\n- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32\\n- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format\\n- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context\\n- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list\\n- nostr_admin_notes — Build admin notes context block from cached kind 1 notes\\n- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata\\n- nostr_admin_relays — Build admin relay context block from cached kind 10002 data\\n- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list\\n- nostr_agent_notes — Build agent notes context block from cached kind 1 notes\\n- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata\\n- nostr_agent_relays — Build agent relay context block from cached kind 10002 data\\n- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default\\n- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache\\n- nostr_decode — Decode a Nostr bech32/nostr: URI into components\\n- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender\\n- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)\\n- nostr_dm_history — Fetch recent DM history for template/context usage; JSON: {\\\"limit\\\":12,\\\"include_current\\\":false} or with explicit peer {\\\"peer_pubkey\\\":\\\"\\\",\\\"limit\\\":20}; template usage: {{nostr_dm_history({\\\"limit\\\":12})}}\\n- nostr_dm_send — Send a NIP-04 encrypted DM\\n- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol\\n- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)\\n- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient\\n- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename\\n- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)\\n- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence\\n- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)\\n- nostr_npub — Return this agent's pubkey encoded as npub bech32\\n- nostr_post — Publish a Nostr event to connected relays\\n- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md\\n- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey\\n- nostr_pubkey — Return this agent's pubkey in hex format\\n- nostr_query — Query events from relays using a Nostr filter\\n- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)\\n- nostr_relay_info — Fetch NIP-11 relay information document\\n- nostr_relay_status — Get connection status and statistics for all relays\\n- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)\\n- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters\\n- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list\\n- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it\\n- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124\\n- skill_get — Get full skill JSON by d tag from local cache; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"} (slash shorthand also works: /skill_get default_admin_dm)\\n- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters\\n- skill_remove — Remove a skill address from kind 10123 adoption list\\n- skill_search — Search public skills by query/author and optionally rank by adoption popularity\\n- skill_set — Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\\\"skill\\\":{\\\"d\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"content\\\":\\\"...\\\",\\\"tags\\\":[[\\\"d\\\",\\\"default_admin_dm\\\"],[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"]]}}\\n- skill_view — Compatibility alias for skill_get; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"}\\n- task_list — Build current task list context block from agent task memory on Nostr\\n- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace\\n- tool_list — List available tools with name, description, and JSON parameter schema\\n- trigger_list — List active triggered skills and their runtime status\\n\\nAVAILABLE SKILLS\\n- default_admin_dm — Default admin DM handler\\n\\nADOPTED SKILLS\\n- default_admin_dm\\n\",\"created_at\":1774222361},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222404},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222404},{\"role\":\"user\",\"content\":\"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\\n\\n /skill_get default_admin_dm\\n7922 3/22/2026, 7:33:24 PM\\nkind4-nip04\\n⋯\\n\\n{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\"from\\\":\\\"admin\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222576}]"},{"role":"user","content":"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\n\n /skill_get default_admin_dm\n7922 3/22/2026, 7:33:24 PM\nkind4-nip04\n⋯\n\n{\"success\":true,\"d_tag\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"owner\":\"agent\",\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"created_at\":1774222308,\"content\":\"You are {{my_kind0_profile}}\\n\\nYour npub: {{my_npub}}\\n\\n## Rules\\n\\n- Communicate through encrypted Nostr direct messages\\n- Keep responses concise and clear\\n- Be helpful and technically accurate\\n- If unsure, state uncertainty directly\\n- Use tools when a request requires taking action\\n- After a tool call, base your answer on the actual tool result\\n- Never claim a tool was run if no tool was executed\\n- Maintain your task list as short-term working memory\\n- Never reveal your private key (nsec)\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\",\"tags\":[[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\"from\":\"admin\"}\"],[\"d\",\"default_admin_dm\"]]}","_ts":1774222576},{"role":"assistant","content":"\n\nYou're right — that `id` of all zeros looks suspicious. Let me query the relays directly for this event.","tool_calls":[{"id":"toolu_bdrk_01Lo8nHAHmPPhMzDxSp1CLXr","type":"function","function":{"name":"my_pubkey","arguments":"{}"}}]},{"role":"tool","tool_call_id":"toolu_bdrk_01Lo8nHAHmPPhMzDxSp1CLXr","content":"{\"success\":true,\"pubkey\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\",\"content\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\"}"},{"role":"assistant","content":null,"tool_calls":[{"id":"toolu_bdrk_01AhjRUEVi3vkcvwPXJ6t1pv","type":"function","function":{"name":"nostr_query","arguments":"{\"filter\": {\"authors\":[\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\"],\"kinds\":[31124],\"#d\":[\"default_admin_dm\"],\"limit\":5}}"}}]},{"role":"tool","tool_call_id":"toolu_bdrk_01AhjRUEVi3vkcvwPXJ6t1pv","content":"{\"success\":true,\"events\":[{\"pubkey\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\",\"created_at\":1774221663,\"kind\":31124,\"tags\":[[\"description\",\"Default admin DM handler\"],[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"],[\"action\",\"llm\"],[\"enabled\",\"true\"]],\"content\":\"You are {{my_kind0_profile}}\\n\\nYour npub: {{my_npub}}\\n\\n## Rules\\n\\n- Communicate through encrypted Nostr direct messages\\n- Keep responses concise and clear\\n- Be helpful and technically accurate\\n- If unsure, state uncertainty directly\\n- Use tools when a request requires taking action\\n- After a tool call, base your answer on the actual tool result\\n- Never claim a tool was run if no tool was executed\\n- Maintain your task list as short-term working memory\\n- Never reveal your private key (nsec)\\n- You may share your public key (npub) with anyone\\n\\n{{nostr_dm_history}}\",\"id\":\"b874122d049aaade3b802db9e80119db0b5c337adb85bd8ba65464d99e44c9a1\",\"sig\":\"5c16bdc4306953604ed4b85e7aa5fb684dcf2c8858ba72fa32a9d26556682e73058b3a1c9077c1c4ce6e4d6ba88d076f5d611ca78050a2a2cfafe318f621e7b9\"},{\"content\":\"You are {{my_kind0_profile}}\\n\\nYour npub: {{my_npub}}\\n\\n## Rules\\n\\n- Communicate through encrypted Nostr direct messages\\n- Keep responses concise and clear\\n- Be helpful and technically accurate\\n- If unsure, state uncertainty directly\\n- Use tools when a request requires taking action\\n- After a tool call, base your answer on the actual tool result\\n- Never claim a tool was run if no tool was executed\\n- Maintain your task list as short-term working memory\\n- Never reveal your private key (nsec)\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\",\"created_at\":1774222308,\"id\":\"bf7f93366ef3a70f30fa64c405631325f58c7f2a15d9217d6b14558ca2189ef7\",\"kind\":31124,\"pubkey\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\",\"sig\":\"b3d9c15a75badcad501d08416a6d3bf3847cd0a5ec9b1425fbbaf8a02f1e947cf0ba7c91ccc863011be9b1c9e33429db4d82013edbf9e6f8e01df11717b33527\",\"tags\":[[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"],[\"d\",\"default_admin_dm\"]]}]}"}] + +--- + +```text +Context Log - seen by model +timestamp=2026-03-22 19:36:20 +phase=llm_chat_with_tools_messages +sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 +model=claude-opus-4.6 +context_bytes=14107 +approx_tokens=3526 +``` + +[{"role":"system","content":"You are {\"name\":\"Didactyl Agent\",\"display_name\":\"Didactyl\",\"about\":\"A sovereign AI agent on Nostr\",\"picture\":\"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\"banner\":\"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324},{\"role\":\"assistant\",\"content\":\"Hello! 👋 How can I help you today? I'm the Didactyl Agent — I can answer questions about the Didactyl project, help with Nostr-related tasks, or assist with anything else you need. Just let me know!\",\"created_at\":1774222330},{\"role\":\"user\",\"content\":\"/help\",\"created_at\":1774222358},{\"role\":\"assistant\",\"content\":\"AVAILABLE TOOLS\\n(run / -h for full JSON args template)\\n- admin_identity — Build admin identity context block from cached runtime metadata\\n- agent_identity — Build agent identity context block with pubkey and npub\\n- agent_version — Return current Didactyl version and metadata from build macros\\n- blossom_delete — Delete a blob from Blossom\\n- blossom_download — Download a blob from Blossom to a local file\\n- blossom_head — Fetch blob metadata from Blossom\\n- blossom_list — List blobs for a pubkey on Blossom\\n- blossom_upload — Upload a local file to a Blossom server\\n- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit\\n- cashu_wallet_check_proofs — Check current wallet proofs state against mint\\n- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint\\n- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id\\n- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request\\n- cashu_wallet_mint_check — Check a previously requested mint quote status\\n- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state\\n- cashu_wallet_mint_quote — Request a mint quote for a target amount\\n- cashu_wallet_mints_get — Get currently configured wallet and public mints\\n- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both\\n- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state\\n- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount\\n- config_recall — Fetch and decrypt agent config kind 30078 by d_tag\\n- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag\\n- local_file_read — Read a local file as text from the configured working directory\\n- local_file_write — Write text content to a local file in the configured working directory\\n- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body\\n- local_shell_exec — Execute a shell command and return stdout/stderr\\n- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)\\n- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed\\n- model_get — Get current active LLM runtime configuration (excluding API key)\\n- model_list — List available model IDs using provider OpenAI-compatible /models endpoint\\n- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)\\n- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context\\n- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context\\n- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context\\n- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32\\n- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format\\n- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context\\n- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list\\n- nostr_admin_notes — Build admin notes context block from cached kind 1 notes\\n- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata\\n- nostr_admin_relays — Build admin relay context block from cached kind 10002 data\\n- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list\\n- nostr_agent_notes — Build agent notes context block from cached kind 1 notes\\n- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata\\n- nostr_agent_relays — Build agent relay context block from cached kind 10002 data\\n- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default\\n- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache\\n- nostr_decode — Decode a Nostr bech32/nostr: URI into components\\n- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender\\n- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)\\n- nostr_dm_history — Fetch recent DM history for template/context usage; JSON: {\\\"limit\\\":12,\\\"include_current\\\":false} or with explicit peer {\\\"peer_pubkey\\\":\\\"\\\",\\\"limit\\\":20}; template usage: {{nostr_dm_history({\\\"limit\\\":12})}}\\n- nostr_dm_send — Send a NIP-04 encrypted DM\\n- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol\\n- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)\\n- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient\\n- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename\\n- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)\\n- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence\\n- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)\\n- nostr_npub — Return this agent's pubkey encoded as npub bech32\\n- nostr_post — Publish a Nostr event to connected relays\\n- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md\\n- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey\\n- nostr_pubkey — Return this agent's pubkey in hex format\\n- nostr_query — Query events from relays using a Nostr filter\\n- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)\\n- nostr_relay_info — Fetch NIP-11 relay information document\\n- nostr_relay_status — Get connection status and statistics for all relays\\n- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)\\n- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters\\n- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list\\n- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it\\n- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124\\n- skill_get — Get full skill JSON by d tag from local cache; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"} (slash shorthand also works: /skill_get default_admin_dm)\\n- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters\\n- skill_remove — Remove a skill address from kind 10123 adoption list\\n- skill_search — Search public skills by query/author and optionally rank by adoption popularity\\n- skill_set — Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\\\"skill\\\":{\\\"d\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"content\\\":\\\"...\\\",\\\"tags\\\":[[\\\"d\\\",\\\"default_admin_dm\\\"],[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"]]}}\\n- skill_view — Compatibility alias for skill_get; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"}\\n- task_list — Build current task list context block from agent task memory on Nostr\\n- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace\\n- tool_list — List available tools with name, description, and JSON parameter schema\\n- trigger_list — List active triggered skills and their runtime status\\n\\nAVAILABLE SKILLS\\n- default_admin_dm — Default admin DM handler\\n\\nADOPTED SKILLS\\n- default_admin_dm\\n\",\"created_at\":1774222361},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222404},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222404},{\"role\":\"user\",\"content\":\"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\\n\\n /skill_get default_admin_dm\\n7922 3/22/2026, 7:33:24 PM\\nkind4-nip04\\n⋯\\n\\n{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\"from\\\":\\\"admin\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222576}]"},{"role":"user","content":"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\n\n /skill_get default_admin_dm\n7922 3/22/2026, 7:33:24 PM\nkind4-nip04\n⋯\n\n{\"success\":true,\"d_tag\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"owner\":\"agent\",\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"created_at\":1774222308,\"content\":\"You are {{my_kind0_profile}}\\n\\nYour npub: {{my_npub}}\\n\\n## Rules\\n\\n- Communicate through encrypted Nostr direct messages\\n- Keep responses concise and clear\\n- Be helpful and technically accurate\\n- If unsure, state uncertainty directly\\n- Use tools when a request requires taking action\\n- After a tool call, base your answer on the actual tool result\\n- Never claim a tool was run if no tool was executed\\n- Maintain your task list as short-term working memory\\n- Never reveal your private key (nsec)\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\",\"tags\":[[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\"from\":\"admin\"}\"],[\"d\",\"default_admin_dm\"]]}","_ts":1774222576},{"role":"assistant","content":"\n\nYou're right — that `id` of all zeros looks suspicious. Let me query the relays directly for this event.","tool_calls":[{"id":"toolu_bdrk_01Lo8nHAHmPPhMzDxSp1CLXr","type":"function","function":{"name":"my_pubkey","arguments":"{}"}}]},{"role":"tool","tool_call_id":"toolu_bdrk_01Lo8nHAHmPPhMzDxSp1CLXr","content":"{\"success\":true,\"pubkey\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\",\"content\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\"}"}] + +--- + +```text +Context Log - seen by model +timestamp=2026-03-22 19:36:16 +phase=llm_chat_with_tools_messages +sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 +model=claude-opus-4.6 +context_bytes=13575 +approx_tokens=3393 +``` + +[{"role":"system","content":"You are {\"name\":\"Didactyl Agent\",\"display_name\":\"Didactyl\",\"about\":\"A sovereign AI agent on Nostr\",\"picture\":\"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\"banner\":\"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324},{\"role\":\"assistant\",\"content\":\"Hello! 👋 How can I help you today? I'm the Didactyl Agent — I can answer questions about the Didactyl project, help with Nostr-related tasks, or assist with anything else you need. Just let me know!\",\"created_at\":1774222330},{\"role\":\"user\",\"content\":\"/help\",\"created_at\":1774222358},{\"role\":\"assistant\",\"content\":\"AVAILABLE TOOLS\\n(run / -h for full JSON args template)\\n- admin_identity — Build admin identity context block from cached runtime metadata\\n- agent_identity — Build agent identity context block with pubkey and npub\\n- agent_version — Return current Didactyl version and metadata from build macros\\n- blossom_delete — Delete a blob from Blossom\\n- blossom_download — Download a blob from Blossom to a local file\\n- blossom_head — Fetch blob metadata from Blossom\\n- blossom_list — List blobs for a pubkey on Blossom\\n- blossom_upload — Upload a local file to a Blossom server\\n- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit\\n- cashu_wallet_check_proofs — Check current wallet proofs state against mint\\n- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint\\n- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id\\n- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request\\n- cashu_wallet_mint_check — Check a previously requested mint quote status\\n- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state\\n- cashu_wallet_mint_quote — Request a mint quote for a target amount\\n- cashu_wallet_mints_get — Get currently configured wallet and public mints\\n- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both\\n- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state\\n- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount\\n- config_recall — Fetch and decrypt agent config kind 30078 by d_tag\\n- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag\\n- local_file_read — Read a local file as text from the configured working directory\\n- local_file_write — Write text content to a local file in the configured working directory\\n- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body\\n- local_shell_exec — Execute a shell command and return stdout/stderr\\n- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)\\n- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed\\n- model_get — Get current active LLM runtime configuration (excluding API key)\\n- model_list — List available model IDs using provider OpenAI-compatible /models endpoint\\n- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)\\n- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context\\n- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context\\n- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context\\n- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32\\n- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format\\n- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context\\n- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list\\n- nostr_admin_notes — Build admin notes context block from cached kind 1 notes\\n- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata\\n- nostr_admin_relays — Build admin relay context block from cached kind 10002 data\\n- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list\\n- nostr_agent_notes — Build agent notes context block from cached kind 1 notes\\n- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata\\n- nostr_agent_relays — Build agent relay context block from cached kind 10002 data\\n- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default\\n- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache\\n- nostr_decode — Decode a Nostr bech32/nostr: URI into components\\n- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender\\n- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)\\n- nostr_dm_history — Fetch recent DM history for template/context usage; JSON: {\\\"limit\\\":12,\\\"include_current\\\":false} or with explicit peer {\\\"peer_pubkey\\\":\\\"\\\",\\\"limit\\\":20}; template usage: {{nostr_dm_history({\\\"limit\\\":12})}}\\n- nostr_dm_send — Send a NIP-04 encrypted DM\\n- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol\\n- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)\\n- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient\\n- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename\\n- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)\\n- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence\\n- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)\\n- nostr_npub — Return this agent's pubkey encoded as npub bech32\\n- nostr_post — Publish a Nostr event to connected relays\\n- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md\\n- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey\\n- nostr_pubkey — Return this agent's pubkey in hex format\\n- nostr_query — Query events from relays using a Nostr filter\\n- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)\\n- nostr_relay_info — Fetch NIP-11 relay information document\\n- nostr_relay_status — Get connection status and statistics for all relays\\n- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)\\n- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters\\n- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list\\n- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it\\n- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124\\n- skill_get — Get full skill JSON by d tag from local cache; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"} (slash shorthand also works: /skill_get default_admin_dm)\\n- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters\\n- skill_remove — Remove a skill address from kind 10123 adoption list\\n- skill_search — Search public skills by query/author and optionally rank by adoption popularity\\n- skill_set — Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\\\"skill\\\":{\\\"d\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"content\\\":\\\"...\\\",\\\"tags\\\":[[\\\"d\\\",\\\"default_admin_dm\\\"],[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"]]}}\\n- skill_view — Compatibility alias for skill_get; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"}\\n- task_list — Build current task list context block from agent task memory on Nostr\\n- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace\\n- tool_list — List available tools with name, description, and JSON parameter schema\\n- trigger_list — List active triggered skills and their runtime status\\n\\nAVAILABLE SKILLS\\n- default_admin_dm — Default admin DM handler\\n\\nADOPTED SKILLS\\n- default_admin_dm\\n\",\"created_at\":1774222361},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222404},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222404},{\"role\":\"user\",\"content\":\"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\\n\\n /skill_get default_admin_dm\\n7922 3/22/2026, 7:33:24 PM\\nkind4-nip04\\n⋯\\n\\n{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You are {{my_kind0_profile}}\\\\n\\\\nYour npub: {{my_npub}}\\\\n\\\\n## Rules\\\\n\\\\n- Communicate through encrypted Nostr direct messages\\\\n- Keep responses concise and clear\\\\n- Be helpful and technically accurate\\\\n- If unsure, state uncertainty directly\\\\n- Use tools when a request requires taking action\\\\n- After a tool call, base your answer on the actual tool result\\\\n- Never claim a tool was run if no tool was executed\\\\n- Maintain your task list as short-term working memory\\\\n- Never reveal your private key (nsec)\\\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\"from\\\":\\\"admin\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222576}]"},{"role":"user","content":"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\n\n /skill_get default_admin_dm\n7922 3/22/2026, 7:33:24 PM\nkind4-nip04\n⋯\n\n{\"success\":true,\"d_tag\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"owner\":\"agent\",\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"created_at\":1774222308,\"content\":\"You are {{my_kind0_profile}}\\n\\nYour npub: {{my_npub}}\\n\\n## Rules\\n\\n- Communicate through encrypted Nostr direct messages\\n- Keep responses concise and clear\\n- Be helpful and technically accurate\\n- If unsure, state uncertainty directly\\n- Use tools when a request requires taking action\\n- After a tool call, base your answer on the actual tool result\\n- Never claim a tool was run if no tool was executed\\n- Maintain your task list as short-term working memory\\n- Never reveal your private key (nsec)\\n- You may share your public key (npub) with anyone {{nostr_dm_history}}\",\"tags\":[[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\"from\":\"admin\"}\"],[\"d\",\"default_admin_dm\"]]}","_ts":1774222576}] + +--- + +```text +Context Log - seen by model +timestamp=2026-03-22 19:32:06 +phase=llm_chat_with_tools_messages +sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 +model=claude-opus-4.6 +context_bytes=1526 +approx_tokens=381 +``` + +[{"role":"system","content":"You are {\n \"name\": \"Didactyl Agent\",\n \"display_name\": \"Didactyl Agent\",\n \"about\": \"A sovereign AI agent on Nostr.\\n\\nMission: Help maintain and answer questions about the Didactyl project.\\n\\nhttps://git.laantungir.net/laantungir/didactyl\\n\\nNot your keys, not your Bitcoin.\\nNot your keys, not your Agent.\\nNot your keys, not your Robot.\",\n \"picture\": \"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\n \"banner\": \"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"\n}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324}]"},{"role":"user","content":"hello","_ts":1774222326}] --- diff --git a/plans/strict_startup_validation.md b/plans/strict_startup_validation.md new file mode 100644 index 0000000..7f815cf --- /dev/null +++ b/plans/strict_startup_validation.md @@ -0,0 +1,224 @@ +# 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()` | diff --git a/src/agent.c b/src/agent.c index e1d7d3d..b44ee78 100644 --- a/src/agent.c +++ b/src/agent.c @@ -945,7 +945,11 @@ static __attribute__((unused)) void template_emit_hook(const char* section_name, } static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) { - if (phase && strcmp(phase, "direct_tool_exec") == 0) { + if (!phase) { + return; + } + if (strcmp(phase, "llm_chat_with_tools_messages") != 0 && + strcmp(phase, "llm_trigger_with_tools_messages") != 0) { return; } @@ -970,7 +974,7 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase, int entry_len = snprintf(NULL, 0, "```text\n" - "Context Log - not seen by model\n" + "Context Log - seen by model\n" "timestamp=%s\n" "phase=%s\n" "sender=%s\n" @@ -998,7 +1002,7 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase, snprintf(entry, (size_t)entry_len + 1U, "```text\n" - "Context Log - not seen by model\n" + "Context Log - seen by model\n" "timestamp=%s\n" "phase=%s\n" "sender=%s\n" @@ -1542,56 +1546,6 @@ static int refresh_adopted_skills_cache_if_needed(void) { cJSON_Delete(adoption_events); - if (tmp_count == 0 && g_cfg->startup_event_count > 0 && g_cfg->startup_events) { - for (int i = 0; i < g_cfg->startup_event_count && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) { - startup_event_t* se = &g_cfg->startup_events[i]; - if (!se || (se->kind != 31123 && se->kind != 31124) || !se->content || se->content[0] == '\0') { - continue; - } - - char d_tag_val[65] = {0}; - if (se->tags_json) { - cJSON* tags = cJSON_Parse(se->tags_json); - cJSON* d_tag = tags ? find_tag_value_string_local(tags, "d") : NULL; - if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && d_tag->valuestring[0] != '\0') { - snprintf(d_tag_val, sizeof(d_tag_val), "%s", d_tag->valuestring); - } - cJSON_Delete(tags); - } - if (d_tag_val[0] == '\0') { - snprintf(d_tag_val, sizeof(d_tag_val), "startup-skill-%d", i + 1); - } - - tmp[tmp_count].kind = se->kind; - snprintf(tmp[tmp_count].author_pubkey_hex, - sizeof(tmp[tmp_count].author_pubkey_hex), - "%s", - g_cfg->keys.public_key_hex[0] != '\0' - ? g_cfg->keys.public_key_hex - : "unknown"); - snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", d_tag_val); - tmp[tmp_count].content = strdup(se->content); - tmp[tmp_count].has_trigger = 0; - tmp[tmp_count].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION; - tmp[tmp_count].filter_json[0] = '\0'; - if (se->tags_json && se->tags_json[0] != '\0') { - cJSON* tags = cJSON_Parse(se->tags_json); - cJSON* trigger_tag = tags ? find_tag_value_string_local(tags, "trigger") : NULL; - cJSON* filter_tag = tags ? find_tag_value_string_local(tags, "filter") : NULL; - if (trigger_tag && cJSON_IsString(trigger_tag) && trigger_tag->valuestring && trigger_tag->valuestring[0] != '\0' && - filter_tag && cJSON_IsString(filter_tag) && filter_tag->valuestring && filter_tag->valuestring[0] != '\0') { - tmp[tmp_count].has_trigger = 1; - tmp[tmp_count].trigger_type = trigger_type_from_string(trigger_tag->valuestring); - snprintf(tmp[tmp_count].filter_json, sizeof(tmp[tmp_count].filter_json), "%s", filter_tag->valuestring); - } - cJSON_Delete(tags); - } - if (tmp[tmp_count].content) { - tmp_count++; - } - } - } - pthread_mutex_lock(&g_adopted_skills_mutex); clear_adopted_skills_cache_locked(); for (int i = 0; i < tmp_count; i++) { diff --git a/src/main.c b/src/main.c index ffcf396..1bfbc70 100644 --- a/src/main.c +++ b/src/main.c @@ -1374,34 +1374,74 @@ int main(int argc, char** argv) { startup_step_begin(14, "Subscribe self-skill cache"); DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin"); if (nostr_handler_subscribe_self_skills() != 0) { - DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)"); + startup_step_fail(14, "Subscribe self-skill cache", "nostr_handler_subscribe_self_skills failed"); + DEBUG_ERROR("[didactyl] startup phase: subscribe self skill cache failed"); + fprintf(stderr, "Startup aborted: failed to subscribe self-skill cache\n"); + agent_cleanup(); + nostr_block_list_cleanup(); + nostr_handler_cleanup(); + llm_cleanup(); + trigger_manager_cleanup(&trigger_manager); + config_free(&cfg); + nostr_cleanup(); + return 1; } - DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end"); - startup_step_ok(14, "Subscribe self-skill cache", NULL); - char startup_dm[768]; - const char* startup_name = nostr_handler_get_startup_display_name(); - int connected_relays = nostr_handler_connected_relay_count(); - time_t startup_now = time(NULL); - struct tm startup_tm; - char startup_time_str[32] = {0}; - if (localtime_r(&startup_now, &startup_tm)) { - (void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm); - } else { - snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now); + const int self_skill_eose_timeout_ms = 15000; + if (nostr_handler_wait_for_self_skill_eose(self_skill_eose_timeout_ms) != 0) { + startup_step_fail(14, + "Subscribe self-skill cache", + "self-skill EOSE not received within timeout"); + fprintf(stderr, + "Startup aborted: self-skill EOSE not received within %d ms\n", + self_skill_eose_timeout_ms); + fprintf(stderr, + "Check relay connectivity (connected=%d/%d) and ensure skill events exist on relays\n", + nostr_handler_connected_relay_count(), + cfg.relay_count); + fprintf(stderr, + "If this is a first run, use genesis to publish startup events\n"); + agent_cleanup(); + nostr_block_list_cleanup(); + nostr_handler_cleanup(); + llm_cleanup(); + trigger_manager_cleanup(&trigger_manager); + config_free(&cfg); + nostr_cleanup(); + return 1; } - snprintf(startup_dm, - sizeof(startup_dm), - "%s has started up and is online at %s (version %s, connected relays: %d/%d).", - (startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl", - startup_time_str, - DIDACTYL_VERSION, - connected_relays, - cfg.relay_count); - if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) { - DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin"); + + int startup_skill_count = 0; + int startup_adoption_count = 0; + if (nostr_handler_validate_self_skill_cache(&startup_skill_count, &startup_adoption_count) != 0) { + startup_step_fail(14, + "Subscribe self-skill cache", + "no skill events found after EOSE"); + fprintf(stderr, + "Startup aborted: no skill events (kind 31123/31124) found in self-skill cache\n"); + fprintf(stderr, + "The agent requires at least one skill event to operate\n"); + fprintf(stderr, + "Run genesis to publish the default skill, or publish a skill manually\n"); + agent_cleanup(); + nostr_block_list_cleanup(); + nostr_handler_cleanup(); + llm_cleanup(); + trigger_manager_cleanup(&trigger_manager); + config_free(&cfg); + nostr_cleanup(); + return 1; } + char self_skill_detail[128]; + snprintf(self_skill_detail, + sizeof(self_skill_detail), + "skills=%d adoptions=%d", + startup_skill_count, + startup_adoption_count); + DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end"); + startup_step_ok(14, "Subscribe self-skill cache", self_skill_detail); + startup_step_begin(15, "Subscribe DMs"); DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin"); if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) { @@ -1497,6 +1537,29 @@ int main(int argc, char** argv) { nostr_handler_refresh_relay_statuses(); + char startup_dm[768]; + const char* startup_name = nostr_handler_get_startup_display_name(); + int connected_relays = nostr_handler_connected_relay_count(); + time_t startup_now = time(NULL); + struct tm startup_tm; + char startup_time_str[32] = {0}; + if (localtime_r(&startup_now, &startup_tm)) { + (void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm); + } else { + snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now); + } + snprintf(startup_dm, + sizeof(startup_dm), + "%s has started up and is online at %s (version %s, connected relays: %d/%d).", + (startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl", + startup_time_str, + DIDACTYL_VERSION, + connected_relays, + cfg.relay_count); + if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) { + DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin"); + } + startup_step_ok(18, "READY", "agent online; entering main poll loop"); DEBUG_INFO("[didactyl] entering main poll loop"); DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex); diff --git a/src/main.h b/src/main.h index 15631f7..874ab6d 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 2 -#define DIDACTYL_VERSION_PATCH 8 -#define DIDACTYL_VERSION "v0.2.8" +#define DIDACTYL_VERSION_PATCH 9 +#define DIDACTYL_VERSION "v0.2.9" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index 0fc50ba..9b3e51d 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -96,6 +96,7 @@ static external_subscription_wrapper_t* g_external_sub_wrappers = NULL; static cJSON* g_self_skill_events = NULL; static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER; +static volatile int g_self_skill_eose_received = 0; #define DM_DEDUP_CACHE_SIZE 256 @@ -448,7 +449,6 @@ static int managed_subscription_register_locked(managed_subscription_id_t id, static int managed_subscriptions_resubscribe_all_locked(void); static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id); static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label); -static void seed_default_skill_into_cache(void); static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url) { if (!relays || relay_count <= 0 || !relay_url || relay_url[0] == '\0') { @@ -1296,50 +1296,6 @@ static void self_skill_cache_upsert_event_locked(cJSON* event) { cJSON_AddItemToArray(g_self_skill_events, dup); } -static void seed_default_skill_into_cache(void) { - if (!g_cfg) { - return; - } - - if (g_cfg->default_skill.kind != 31123 && g_cfg->default_skill.kind != 31124) { - return; - } - - if (g_cfg->keys.public_key_hex[0] == '\0' || - !g_cfg->default_skill.content || g_cfg->default_skill.content[0] == '\0' || - !g_cfg->default_skill.tags_json || g_cfg->default_skill.tags_json[0] == '\0') { - return; - } - - cJSON* tags = cJSON_Parse(g_cfg->default_skill.tags_json); - if (!tags || !cJSON_IsArray(tags)) { - cJSON_Delete(tags); - return; - } - - cJSON* event = cJSON_CreateObject(); - if (!event) { - cJSON_Delete(tags); - return; - } - - const double now = (double)time(NULL); - cJSON_AddNumberToObject(event, "kind", g_cfg->default_skill.kind); - cJSON_AddStringToObject(event, "pubkey", g_cfg->keys.public_key_hex); - cJSON_AddStringToObject(event, "content", g_cfg->default_skill.content); - cJSON_AddItemToObject(event, "tags", tags); - cJSON_AddNumberToObject(event, "created_at", now); - cJSON_AddStringToObject(event, - "id", - "0000000000000000000000000000000000000000000000000000000000000000"); - - pthread_mutex_lock(&g_self_skill_mutex); - self_skill_cache_upsert_event_locked(event); - pthread_mutex_unlock(&g_self_skill_mutex); - - cJSON_Delete(event); -} - static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) { if (!hex || !out_pubkey || strlen(hex) != 64U) { return -1; @@ -1753,6 +1709,7 @@ static void on_eose(cJSON** events, int event_count, void* user_data) { static void on_self_skill_eose(cJSON** events, int event_count, void* user_data) { (void)events; (void)user_data; + g_self_skill_eose_received = 1; DEBUG_INFO("[didactyl] self-skill EOSE received: cached skill events=%d", event_count); if (g_self_skill_eose_cb) { @@ -2162,7 +2119,6 @@ static void on_agent_context_event(cJSON* event, const char* relay_url, void* us } static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) { - (void)relay_url; (void)user_data; if (!event || !g_cfg) { @@ -2173,6 +2129,20 @@ static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_ return; } + cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); + cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); + cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id"); + cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); + int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1; + const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL; + + DEBUG_INFO("[didactyl] self-skill cache received kind=%d d_tag=%s id=%s created_at=%lld via %s", + kind_val, + (d_tag && d_tag[0] != '\0') ? d_tag : "-", + (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "", + (long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0), + relay_url ? relay_url : "unknown relay"); + register_trigger_from_self_skill_event(event); pthread_mutex_lock(&g_self_skill_mutex); @@ -2470,6 +2440,7 @@ int nostr_handler_subscribe_self_skills(void) { } int rc = 0; + g_self_skill_eose_received = 0; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); @@ -2719,6 +2690,77 @@ void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callb g_self_skill_eose_user_data = user_data; } +int nostr_handler_wait_for_self_skill_eose(int timeout_ms) { + if (!g_pool || timeout_ms <= 0) { + return -1; + } + + struct timespec ts_start; + if (clock_gettime(CLOCK_MONOTONIC, &ts_start) != 0) { + return -1; + } + + while (!g_self_skill_eose_received) { + (void)nostr_handler_poll(100); + + struct timespec ts_now; + if (clock_gettime(CLOCK_MONOTONIC, &ts_now) != 0) { + return -1; + } + long long elapsed_ms = (long long)(ts_now.tv_sec - ts_start.tv_sec) * 1000LL + + (long long)(ts_now.tv_nsec - ts_start.tv_nsec) / 1000000LL; + if (elapsed_ms >= (long long)timeout_ms) { + return -1; + } + } + + return 0; +} + +int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count) { + int skill_count = 0; + int adoption_count = 0; + + pthread_mutex_lock(&g_self_skill_mutex); + if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) { + int n = cJSON_GetArraySize(g_self_skill_events); + for (int i = 0; i < n; i++) { + cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i); + cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL; + cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL; + cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL; + cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL; + if (!kind || !cJSON_IsNumber(kind)) { + continue; + } + + int k = (int)kind->valuedouble; + if (k == 31123 || k == 31124) { + skill_count++; + } else if (k == 10123) { + adoption_count++; + } + + const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL; + DEBUG_INFO("[didactyl] self-skill cache event: kind=%d d_tag=%s id=%s created_at=%lld", + k, + (d_tag && d_tag[0] != '\0') ? d_tag : "-", + (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "", + (long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0)); + } + } + pthread_mutex_unlock(&g_self_skill_mutex); + + if (out_skill_count) { + *out_skill_count = skill_count; + } + if (out_adoption_count) { + *out_adoption_count = adoption_count; + } + + return skill_count > 0 ? 0 : -1; +} + int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) { if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) { return -1; @@ -3134,6 +3176,19 @@ static void publish_pending_startup_events_for_relay_index(int relay_index, cons } } +static const char* startup_kind_label(int kind) { + switch (kind) { + case 0: return "profile"; + case 1: return "note"; + case 3: return "contacts"; + case 10002: return "relay_list"; + case 10123: return "adoption_list"; + case 31123: return "private_skill"; + case 31124: return "skill"; + default: return "event"; + } +} + int nostr_handler_reconcile_startup_events(void) { if (!g_cfg || !g_pool) { return -1; @@ -3162,7 +3217,54 @@ int nostr_handler_reconcile_startup_events(void) { publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile"); } - seed_default_skill_into_cache(); + if (g_startup_publish_tracking_enabled && g_startup_published) { + int events_published = 0; + int relays_used = 0; + + for (int r = 0; r < g_cfg->relay_count; r++) { + int used = 0; + for (int i = 0; i < g_cfg->startup_event_count; i++) { + size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r; + if (g_startup_published[slot]) { + used = 1; + break; + } + } + if (used) { + relays_used++; + } + } + + for (int i = 0; i < g_cfg->startup_event_count; i++) { + startup_event_t* se = &g_cfg->startup_events[i]; + char relays_line[512]; + relays_line[0] = '\0'; + int relay_hits = 0; + for (int r = 0; r < g_cfg->relay_count; r++) { + size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r; + if (!g_startup_published[slot]) { + continue; + } + relay_hits++; + if (relays_line[0] != '\0') { + strncat(relays_line, ", ", sizeof(relays_line) - strlen(relays_line) - 1U); + } + strncat(relays_line, g_cfg->relays[r], sizeof(relays_line) - strlen(relays_line) - 1U); + } + if (relay_hits > 0) { + events_published++; + DEBUG_INFO("[didactyl] startup publish detail: kind=%d (%s) -> %s", + se->kind, + startup_kind_label(se->kind), + relays_line[0] ? relays_line : ""); + } + } + + DEBUG_INFO("[didactyl] startup publish summary: %d event(s) published to %d/%d relay(s)", + events_published, + relays_used, + g_cfg->relay_count); + } return 0; } diff --git a/src/nostr_handler.h b/src/nostr_handler.h index 4f39bb1..f2ee8f9 100644 --- a/src/nostr_handler.h +++ b/src/nostr_handler.h @@ -59,6 +59,8 @@ nostr_pool_subscription_t* nostr_handler_subscribe_with_filter( int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription); int nostr_handler_poll(int timeout_ms); int nostr_handler_reconcile_startup_events(void); +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); void nostr_handler_refresh_relay_statuses(void); int nostr_handler_sync_relays_from_config(void); char* nostr_handler_get_self_skill_events_json(void);