Files
sovereign_browser/plans/cross-project-agent-sync.md

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:

  1. Fails silently (XHR error on sovereign://agents/messages), or
  2. The message count check msgs.length !== lastCount never 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/messages returns 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 expects m.role, m.content, m.tool_calls as fields — verify the store returns these.

Files to modify:


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-mini while the client uses claude-opus-4.6
  • Each app's agent config is independent
  • Other shared settings (zaps, theme, etc.) live alongside global.agent
  • The sovereign://agents config page manages both the shared providers list AND the browser's per-app agent selection

Plan:

  1. Migrate settings_sync.c from d-tag "sovereign_browser" to "user-settings". Change SETTINGS_SYNC_D_TAG to "user-settings". Move the browser-specific settings into a sovereign_browser namespace inside the encrypted JSON (instead of top-level keys).

  2. Read global_llm from d:user-settings on startup. After login, fetch the user's kind 30078 d:user-settings event, NIP-44 decrypt, parse global_llm, and populate browser_settings_t agent fields. Also parse the sovereign_browser namespace for browser settings.

  3. 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_llm or sovereign_browser), re-encrypt, publish. This preserves other apps' namespaces (global_zaps, global_ui, etc.).

  4. Multi-provider support. The global_llm.providers array allows multiple providers. The sovereign://agents config page should let the user manage multiple providers (add/remove/edit), select the active one, and fetch models per provider.

  5. Backward compatibility. On first load after migration, if a d:sovereign_browser event exists but no d:user-settings does, read the old event and migrate the data into the new d:user-settings shape. Then publish the merged event and stop using the old d-tag.

Modified files:

  • src/settings_sync.h — Change SETTINGS_SYNC_D_TAG to "user-settings".
  • src/settings_sync.c — Move browser settings into sovereign_browser namespace, add read-modify-write logic, add global_llm read/write, add backward-compat migration from old d-tag.
  • src/settings.c — After settings_load_user(), call the sync load to fetch from Nostr.
  • src/nostr_bridge.chandle_agents_set() should trigger a debounced sync publish.
  • src/settings.h — Add multi-provider fields to browser_settings_t (providers array, active provider index).
  • src/relay_fetch.c — Update the fetch filter to use d:user-settings instead of d:sovereign_browser.

Workstream 3 — Port ai.html chat UI elements

What ai.html has that we want

From ~/lt/client/www/ai.html:

  1. Chat layout — Left pane with conversation list + skills list, right pane with chat thread + input area. See ai.html:44-80 for the CSS layout.

  2. Conversation persistence to Nostr — Each conversation is saved as a kind 30078 event with d:{conversation-id} and t:client-ai-chat-v1, NIP-44 encrypted. See ai.html:788 and ai.html:1741.

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

  4. 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_tool tags 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 of handle_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 (via relay_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_llm in d: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.