15 KiB
Cross-Project Agent Sync — sovereign_browser ↔ client ↔ didactyl
Goal
Align sovereign_browser's embedded agent with the patterns used in ~/lt/client
(the web app) and ~/lt/didactyl (the C agent daemon), so all three projects
share LLM provider settings, skills, and conversations via Nostr kind 30078
events. Also fix the immediate bug: messages don't appear in the chat UI.
Three workstreams
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ BugFix │ │ SettingsSync │ │ ChatUI │
│ Fix chat UI │────▶│ kind 30078 │────▶│ Port ai.html │
│ message render │ │ d:user-settings │ │ chat layout │
│ │ │ global_llm sync │ │ + Nostr persist │
└─────────────────┘ └──────────────────┘ └──────────────────┘
Workstream 1 — Fix chat UI message rendering (immediate bug)
Problem: The user sends "hello", sees the response in the console log, but nothing appears in the chat box — neither the user message nor the response.
Root cause: The chat UI's fetchMessages() calls
sovereign://agents/messages which returns the message history as JSON. The
renderMessages() function renders them. But the polling loop calls
updateStatus() and fetchMessages() every 500ms. The status polling works
(state transitions to complete), but fetchMessages() either:
- Fails silently (XHR error on
sovereign://agents/messages), or - The message count check
msgs.length !== lastCountnever triggers because the messages endpoint returns an empty array or the wrong format.
Fix approach:
- Add console.error logging to
fetchMessages()to see the actual XHR response. - Verify
sovereign://agents/messagesreturns the correct JSON array format. - Check that
agent_chat_store_get_messages()returns data in the format the UI expects ([{role, content, tool_calls, tool_call_id}, ...]). - The
renderMessage()function expectsm.role,m.content,m.tool_callsas fields — verify the store returns these.
Files to modify:
src/nostr_bridge.c—handle_agents_chat_page()JS,handle_agents_messages()endpoint.
Workstream 2 — Kind 30078 settings sync (shared LLM config)
What the client does
The client stores all user settings in a single kind 30078 event with
d:user-settings, NIP-44 self-encrypted. The schema (from
~/lt/client/docs/SETTINGS.md):
{
"v": 2,
"updatedAt": 1708646400,
"global_llm": {
"provider": "ppq",
"api_key": "sk-...",
"model": "claude-opus-4.6",
"base_url": "https://api.ppq.ai",
"max_tokens": 200000,
"temperature": 0.7,
"providers": [
{
"name": "ppq",
"base_url": "https://api.ppq.ai",
"api_key": "sk-...",
"models": ["claude-opus-4.6", "claude-haiku-4.5"]
}
],
"favorites": ["claude-opus-4.6"]
}
}
What didactyl does
Didactyl (C agent daemon) stores the same global_llm shape in its own
pubkey's d:user-settings event. See
~/lt/client/docs/SETTINGS.md.
What sovereign_browser should do — Option A: Merge into d:user-settings
sovereign_browser already has kind 30078 settings sync via
settings_sync.c, but it currently uses d-tag
"sovereign_browser". We will migrate to the shared d:user-settings
event, putting browser-specific settings under a sovereign_browser namespace
inside the same encrypted JSON. This aligns with the client and didactyl, and
preserves privacy (no app-specific d-tag leaking which app the user runs).
Target d:user-settings shape — global vs per-app:
The global namespace holds all shared settings — things that should be
the same across every app the user runs. This includes LLM provider
infrastructure (under global.agent), and will grow to include zaps, UI
preferences, relay lists, experimental flags, etc. Each app then has its own
top-level namespace for app-specific config.
{
"v": 2,
"updatedAt": 1708646400,
"global": {
"agent": {
"providers": [
{
"name": "ppq",
"base_url": "https://api.ppq.ai",
"api_key": "sk-...",
"models": ["gpt-4o-mini", "claude-haiku-4.5", "claude-opus-4.6"]
},
{
"name": "ollama-local",
"base_url": "http://localhost:11434",
"api_key": "",
"models": ["llama3.1", "qwen2.5"]
}
],
"favorites": ["gpt-4o-mini", "claude-opus-4.6"]
},
"zaps": {
"defaultAmountSats": 21,
"defaultComment": "",
"preferredMethod": "auto"
},
"ui": {
"theme": "dark",
"language": "en"
},
"relays": { },
"experimental": { }
},
"sovereign_browser": {
"agent": {
"provider": "ppq",
"model": "gpt-4o-mini",
"system_prompt": "",
"max_iterations": 100
},
"new_tab_url": "",
"tab_bar_position": 0,
"max_tabs": 50,
"bootstrap_relays": "...",
"search_engine": "duckduckgo",
"theme_dark": false,
"shortcuts": { ... }
},
"client": {
"ai": {
"provider": "ppq",
"model": "claude-opus-4.6",
"routstr_balance": 0
}
},
"didactyl": {
"agent": {
"provider": "ppq",
"model": "claude-haiku-4.5",
"admin_pubkey": "npub1...",
"dm_protocol": "nip04"
}
}
}
Key principle: global.agent.providers is the shared catalog (API keys,
base URLs, model lists). Each app's namespace (sovereign_browser.agent,
client.ai, didactyl.agent) picks which provider+model to use, and holds
app-specific agent config (system prompt, max_iterations, etc.). The global
namespace will grow over time to include other shared settings (zaps, UI,
relays). This way:
- API keys are entered once, shared everywhere
- The browser can use
gpt-4o-miniwhile the client usesclaude-opus-4.6 - Each app's agent config is independent
- Other shared settings (zaps, theme, etc.) live alongside
global.agent - The
sovereign://agentsconfig page manages both the shared providers list AND the browser's per-app agent selection
Plan:
-
Migrate
settings_sync.cfrom d-tag"sovereign_browser"to"user-settings". ChangeSETTINGS_SYNC_D_TAGto"user-settings". Move the browser-specific settings into asovereign_browsernamespace inside the encrypted JSON (instead of top-level keys). -
Read
global_llmfromd:user-settingson startup. After login, fetch the user's kind 30078d:user-settingsevent, NIP-44 decrypt, parseglobal_llm, and populatebrowser_settings_tagent fields. Also parse thesovereign_browsernamespace for browser settings. -
Read-modify-write on every settings change. When any settings change (browser settings OR agent LLM config), do a read-modify-write: fetch the current event, decrypt, patch the relevant namespace (
global_llmorsovereign_browser), re-encrypt, publish. This preserves other apps' namespaces (global_zaps,global_ui, etc.). -
Multi-provider support. The
global_llm.providersarray allows multiple providers. Thesovereign://agentsconfig page should let the user manage multiple providers (add/remove/edit), select the active one, and fetch models per provider. -
Backward compatibility. On first load after migration, if a
d:sovereign_browserevent exists but nod:user-settingsdoes, read the old event and migrate the data into the newd:user-settingsshape. Then publish the merged event and stop using the old d-tag.
Modified files:
src/settings_sync.h— ChangeSETTINGS_SYNC_D_TAGto"user-settings".src/settings_sync.c— Move browser settings intosovereign_browsernamespace, add read-modify-write logic, addglobal_llmread/write, add backward-compat migration from old d-tag.src/settings.c— Aftersettings_load_user(), call the sync load to fetch from Nostr.src/nostr_bridge.c—handle_agents_set()should trigger a debounced sync publish.src/settings.h— Add multi-provider fields tobrowser_settings_t(providers array, active provider index).src/relay_fetch.c— Update the fetch filter to used:user-settingsinstead ofd:sovereign_browser.
Workstream 3 — Port ai.html chat UI elements
What ai.html has that we want
From ~/lt/client/www/ai.html:
-
Chat layout — Left pane with conversation list + skills list, right pane with chat thread + input area. See
ai.html:44-80for the CSS layout. -
Conversation persistence to Nostr — Each conversation is saved as a kind 30078 event with
d:{conversation-id}andt:client-ai-chat-v1, NIP-44 encrypted. Seeai.html:788andai.html:1741. -
Skills (kind 31123) — Public skills that define system prompts, LLM params, and tool requirements. Users can select multiple skills whose templates are concatenated into the system prompt. See
~/lt/client/plans/ai-skills-integration.md. -
Provider config sidenav — Provider dropdown, model dropdown, API key input, Routstr payment integration.
What to port to sovereign://agents/chat
Phase A — Chat layout:
- Two-pane layout: conversation list (left) + chat thread (right).
- Conversation list shows saved conversations with titles, click to load.
- "New Chat" button to start a new conversation.
- Chat thread shows messages with role labels, tool call details collapsible.
- Input area at the bottom with send button.
Phase B — Conversation persistence to Nostr:
- Save each conversation as kind 30078,
d:{conversation-id},t:sovereign-browser-ai-chat-v1, NIP-44 encrypted. - Content:
{ schema: 1, messages: [...], title: "..." }. - Load conversations from Nostr on page load.
- Delete conversations (kind 5 tombstone).
Phase C — Skills (kind 31123):
- Fetch public skills from Nostr (kind 31123 events).
- Display skills in the left pane below conversations.
- Selecting a skill applies its system prompt template.
- Skills with
requires_tooltags are highlighted (sovereign_browser HAS tools, so they're fully usable — unlike ai.html which has no tools).
Phase D — Save skills/agents to Nostr:
- UI to create/edit skills (kind 31123) with system prompt, model, temp.
- Publish to Nostr so they're shared across all three projects.
Files to modify:
src/nostr_bridge.c— Major rewrite ofhandle_agents_chat_page()to include the two-pane layout, conversation list, skills list. New endpoints:sovereign://agents/conversations,sovereign://agents/conversations/new,sovereign://agents/conversations/delete,sovereign://agents/skills,sovereign://agents/skills/save.src/agent_chat_store.c— Add functions to load/save conversations as Nostr events (viarelay_fetch.c).- New:
src/agent_skills.h/src/agent_skills.c— Fetch, parse, and apply kind 31123 skills.
Cross-project harmony
┌─────────────────────────────────┐
│ User's Nostr │
│ │
│ d:user-settings (k30078) │
│ └─ global_llm (NIP-44 enc) │
│ │
│ d:{convo-id} (k30078) │
│ └─ conversations (NIP-44) │
│ │
│ kind 31123 (public skills) │
└────────┬───────────┬────────────┘
│ │
┌───────────────────┼───────────┼───────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────┐ ┌───────────────┐ ┌───────────────┐ ┌──────────┐
│ client │ │ sovereign_ │ │ sovereign_ │ │ didactyl │
│ ai.html │ │ browser │ │ browser │ │ daemon │
│ │ │ agents config │ │ agents/chat │ │ │
│ read/write │ │ read/write │ │ read/write │ │ read/ │
│ global_llm │ │ global_llm │ │ conversations│ │ write │
│ read/write │ │ │ │ fetch/publish│ │ global │
│ conversations │ │ │ │ skills │ │ _llm │
│ publish/fetch │ │ │ │ │ │ fetch │
│ skills │ │ │ │ │ │ skills │
└─────────────────┘ └───────────────┘ └───────────────┘ └──────────┘
All three projects share:
global_llmind:user-settings— provider, API key, model- Conversations in
d:{convo-id}— encrypted chat history - Skills in kind 31123 — public system prompt templates
sovereign_browser's unique advantage: it has browser tools (snapshot,
click, eval) + filesystem/shell tools that ai.html and didactyl don't
have. Skills with requires_tool tags are fully functional in
sovereign_browser.
Phasing
Phase 1 (immediate): Fix chat UI message rendering bug.
Phase 2: Kind 30078 global_llm sync — read from Nostr on startup,
write on config change. Single-provider first.
Phase 3: Port ai.html chat layout — two-pane, conversation list, conversation persistence to Nostr.
Phase 4: Skills (kind 31123) — fetch, display, apply, create.
Phase 5: Multi-provider support in global_llm.