# Browser Didactyl — A Pure-Browser Agent Runtime

## Vision

Port Didactyl's agent loop to JavaScript as a standalone web page. The browser
becomes the runtime substrate — no shell, no C binary. You open the page, sign
in **as the agent** using the agent's Nostr private key (via
`nostr-login-lite`), and your agent boots: it reads its soul, skills, adoption
list, and memory from relays, listens for triggers, reasons with an LLM, and
acts through a browser-safe tool set.

This is **not** `ai.html` (a chat app for conversations) and **not**
`sovereign_browser` (a full OS browser in C/WebKitGTK). It is Didactyl's agent
semantics — triggers, two-layer context, skill adoption, tool-call loop —
running natively in a browser tab.

---

## Why This Works

Didactyl is Nostr-first. Identity, memory, skills, soul, and communication all
live as Nostr events. The only things tying it to a shell are:

1. **The C binary process** — a browser tab replaces this
2. **Shell-specific tools** — `local_shell_exec`, `local_file_read`,
   `local_file_write`, `local_http_fetch` — these are dropped or replaced with
   browser equivalents

The agent loop itself is portable:

```
Boot → load config from Nostr → subscribe to triggers →
  on trigger: build context → call LLM → execute tools → loop →
  deliver response
```

A browser can do every step:

| Capability | Browser Implementation |
|---|---|
| Nostr relay connection | WebSocket (NDK worker or nostr-tools) |
| Event signing | NIP-07 `window.nostr` or n_signer |
| NIP-04/44 encrypt/decrypt | `window.nostr.nip04` / `window.nostr.nip44` |
| LLM calls | `fetch()` to OpenAI-compatible endpoint |
| Skill/soul/memory storage | Kind 31123/31124/30078/10123 events |
| Blossom blob storage | HTTP `fetch()` to Blossom servers |
| Tool execution | JS tool dispatch (browser-safe subset) |

---

## Architecture

```mermaid
flowchart TB
    subgraph BrowserTab
        UI[Chat UI Page]
        CORE[Agent Core JS Module]
        TOOLS[Tool Dispatch JS Module]
        CTX[Context Builder JS Module]
    end

    subgraph NostrLayer
        RELAYS[Nostr Relays via WebSocket]
        EVENTS[Kind 31123 Skills<br/>Kind 10123 Adoption<br/>Kind 30078 Memory/Config<br/>Kind 4/13 DMs]
    end

    subgraph External
        LLM[OpenAI-compatible LLM API]
        SIGNER[NIP-07 Signer Extension<br/>or n_signer]
        BLOSSOM[Blossom Servers]
    end

    UI --> CORE
    CORE --> CTX
    CORE --> TOOLS
    CORE -->|WebSocket| RELAYS
    RELAYS --> EVENTS
    CORE -->|fetch| LLM
    CORE -->|window.nostr| SIGNER
    TOOLS -->|fetch| BLOSSOM
    TOOLS -->|publish events| RELAYS
```

### Boot Sequence

```mermaid
sequenceDiagram
    participant User
    participant Page as Browser Didactyl
    participant NLL as nostr-login-lite
    participant Signer as window.nostr
    participant Relays as Nostr Relays

    User->>Page: Open page
    Page->>NLL: NOSTR_LOGIN_LITE.init methods: local, extension, connect, nsigner
    NLL->>NLL: Restore persisted auth or show login modal
    User->>NLL: Enter agent nsec or connect signer
    NLL->>NLL: Install window.nostr facade
    Page->>Signer: window.nostr.getPublicKey()
    Signer-->>Page: agent pubkey hex
    Page->>Relays: Connect to configured relays
    Page->>Relays: Subscribe: kind 10002 own pubkey
    Page->>Relays: Subscribe: kind 10123 own pubkey
    Page->>Relays: Subscribe: kinds 31123,31124 own pubkey
    Page->>Relays: Subscribe: kind 30078 own pubkey d=memory
    Page->>Relays: Subscribe: kind 30078 own pubkey d=llm_config
    Page->>Relays: Subscribe: kind 4 own pubkey p-tag
    Relays-->>Page: EOSE + cached events
    Page->>Page: Build adoption list + skill cache
    Page->>Page: Load LLM config from 30078
    Page->>Page: Register trigger subscriptions
    Page-->>User: Agent ready — listening for DMs and triggers
```

---

## Browser-Safe Tool Set

The C agent has ~60 tools. The browser agent starts with a focused subset and
can grow. Tools are grouped by what the browser can actually do.

### Phase 1 — Core Nostr Tools

These are the agent's hands in a browser. All use `window.nostr` for signing and
the WebSocket relay pool for publishing.

| Tool | C Equivalent | Browser Implementation |
|---|---|---|
| `nostr_post` | [`nostr_post`](../src/tools/tool_nostr_post.c:1) | Build event → `window.nostr.signEvent` → publish via relay pool |
| `nostr_query` | [`nostr_query`](../src/tools/tool_nostr_query.c:1) | Send filter over WebSocket, collect until EOSE |
| `nostr_dm_send` | [`nostr_dm_send`](../src/tools/tool_nostr_dm.c:1) | NIP-04 encrypt via `window.nostr.nip04.encrypt` → publish kind 4 |
| `nostr_dm_send_nip17` | [`nostr_dm_send_nip17`](../src/tools/tool_nostr_dm.c:1) | NIP-44 encrypt → gift-wrap kind 13/14 (if signer supports nip44) |
| `nostr_delete` | [`nostr_delete`](../src/tools/tool_nostr_post.c:1) | Sign kind 5 → publish |
| `nostr_react` | [`nostr_react`](../src/tools/tool_nostr_post.c:1) | Sign kind 7 → publish |
| `nostr_profile_get` | [`nostr_profile_get`](../src/tools/tool_nostr_identity.c:1) | Query kind 0 → parse metadata |
| `nostr_encrypt` | [`nostr_encrypt`](../src/tools/tool_signer_crypto.c:1) | `window.nostr.nip44.encrypt` |
| `nostr_decrypt` | [`nostr_decrypt`](../src/tools/tool_signer_crypto.c:1) | `window.nostr.nip44.decrypt` |

### Phase 1 — Identity & Context Tools

| Tool | Browser Implementation |
|---|---|
| `nostr_pubkey` / `my_pubkey` | Return cached agent pubkey hex |
| `nostr_npub` / `my_npub` | bech32 encode via nostr-tools |
| `agent_identity` | Build identity context block |
| `nostr_relay_status` | Query relay pool connection states |
| `tool_list` | Return browser tool schemas |

### Phase 1 — Memory & Task Tools

| Tool | C Equivalent | Browser Implementation |
|---|---|---|
| `memory_save` | [`memory_save`](../src/tools/tool_memory.c:1) | NIP-44 encrypt → publish kind 30078 d=memory |
| `memory_recall` | [`memory_recall`](../src/tools/tool_memory.c:1) | Fetch kind 30078 d=memory → decrypt |
| `task_manage` | [`task_manage`](../src/tools/tool_task.c:1) | Publish/fetch kind 30078 d=tasks |
| `task_list` | [`task_list`](../src/tools/tool_task.c:1) | Fetch kind 30078 d=tasks → format |

### Phase 1 — Skill Tools

| Tool | Browser Implementation |
|---|---|
| `skill_create` | Build kind 31123/31124 → sign → publish |
| `skill_edit` | Fetch existing → modify → republish |
| `skill_list` | Query adopted skills from cache |
| `skill_adopt` | Update kind 10123 → sign → publish |
| `skill_remove` | Update kind 10123 → sign → publish |
| `skill_search` | Query kind 31123 by tag/author |

### Phase 1 — Config & Model Tools

| Tool | Browser Implementation |
|---|---|
| `model_get` | Return active LLM config from 30078 or localStorage |
| `model_set` | Publish kind 30078 d=llm_config |
| `config_store` | Encrypt + publish kind 30078 |
| `config_recall` | Fetch + decrypt kind 30078 |
| `agent_version` | Return hardcoded browser version string |

### Phase 2 — Blossom Tools

| Tool | Browser Implementation |
|---|---|
| `blossom_upload` | `fetch()` PUT to Blossom server with signed auth |
| `blossom_download` | `fetch()` GET from Blossom server |
| `blossom_head` | `fetch()` HEAD |
| `blossom_list` | `fetch()` GET list endpoint |
| `blossom_delete` | `fetch()` DELETE with signed auth |

### Phase 2 — Browser-Native Tools

These have no C equivalent — they are browser-only capabilities:

| Tool | Description |
|---|---|
| `browser_fetch` | `fetch()` any URL — replaces `local_http_fetch` |
| `browser_tab_open` | Open a URL in a new tab |
| `browser_clipboard` | Read/write clipboard |
| `browser_local_storage` | Read/write localStorage key-value pairs |

### Explicitly Excluded

These tools cannot run in a browser and are intentionally absent:

| Tool | Reason |
|---|---|
| `local_shell_exec` | No shell access |
| `local_file_read` | No filesystem (except via File System Access API, Phase 3) |
| `local_file_write` | Same |
| `cashu_wallet_*` | Could be ported later (Cashu is HTTP-based) — Phase 3 |

---

## Context Assembly

The browser agent replicates Didactyl's two-layer context model from
[`docs/CONTEXT.md`](../docs/CONTEXT.md:1) and [`docs/SKILLS.md`](../docs/SKILLS.md:1).

### Two-Layer Model

```mermaid
flowchart TD
    TRIGGER[Trigger fires: DM, cron, subscription]
    TRIGGER --> WALK[Walk adoption list 10123]
    WALK --> MATCH{Skill trigger matches?}
    MATCH -- yes --> L1[Add to Layer 1]
    MATCH -- no --> SKIP[Skip]
    L1 --> RESOLVE[Resolve template variables]
    RESOLVE --> VARTYPE{Variable type?}
    VARTYPE -- tool name --> TOOLEXEC[Execute tool, insert result as markdown]
    VARTYPE -- skill d-tag --> SKILLLOOKUP[Insert adopted skill content - Layer 2]
    VARTYPE -- unknown --> EMPTY[Resolve to empty]
    TOOLEXEC --> FORMAT
    SKILLLOOKUP --> FORMAT
    EMPTY --> FORMAT
    FORMAT[Format document: add title, bump headings, join with ---]
    FORMAT --> ROLES[Split into system/user roles]
    ROLES --> LLM[Send to LLM with tool schemas]
```

### Template Variable Resolution

The C agent resolves `{{variable}}` placeholders in skill templates. The browser
agent does the same:

- `{{skill_d_tag}}` → look up adopted skill, insert content (layer 2)
- `{{tool_name}}` → execute tool, convert JSON result to markdown, insert
- `{{message}}` → the incoming trigger payload (DM text, etc.)
- Unknown → empty string

### Heading Bump

Skill content headings are bumped down one level (`#` → `##`, `##` → `###`)
during assembly, matching [`context_bump_headings()`](../src/context_format.c:1)
in the C agent.

### Role Split

The assembled markdown is split into system/user roles via the same logic as
[`context_roles_split()`](../src/context_roles.c:1). The runtime owns role
construction — skill authors write plain markdown.

---

## Trigger System

The browser agent supports a subset of Didactyl's trigger types:

| Trigger Type | Browser Support | Implementation |
|---|---|---|
| `dm` | Yes | Subscribe to kind 4 events with own pubkey as p-tag |
| `nostr-subscription` | Yes | Register WebSocket subscription with skill's filter |
| `cron` | Yes | `setInterval` / `setTimeout` with cron expression parser |
| `chain` | Yes | Internal event — fire when another skill completes |
| `webhook` | No | No HTTP server in browser (could use a relay-based webhook relay) |

### DM Trigger Flow

```mermaid
sequenceDiagram
    participant Admin
    participant Relays as Nostr Relays
    participant Page as Browser Didactyl
    participant LLM as LLM API

    Admin->>Relays: Publish kind 4 DM to agent
    Relays->>Page: WebSocket event notification
    Page->>Page: NIP-04 decrypt
    Page->>Page: Dedup check event ID cache
    Page->>Page: Classify sender tier admin/wot/stranger
    Page->>Page: Build context from triggered skills
    Page->>LLM: Chat completion with tools
    LLM-->>Page: Response or tool_calls
    loop tool call loop
        Page->>Page: Execute tool
        Page->>LLM: Feed tool result back
        LLM-->>Page: Next response
    end
    Page->>Relays: Publish kind 4 DM reply
    Relays->>Admin: Deliver reply
```

### Cron Trigger

Cron expressions are parsed in JS and scheduled via `setTimeout`. The browser
tab must stay open for cron to fire (a Service Worker could keep it alive in a
future enhancement).

### Sender Tier Classification

The C agent classifies senders as ADMIN / WoT / STRANGER. The browser agent
replicates this:

- **ADMIN** — pubkey matches configured admin pubkey
- **WoT** — pubkey is in admin's kind 3 contact list
- **STRANGER** — everyone else (configurable: ignore or canned reply)

---

## LLM Integration

### Provider Config

LLM config is read from kind 30078 `d:llm_config` under the agent's own pubkey,
falling back to the shared `d:user-settings` `global_llm` namespace (matching
the cross-project settings sync from
[`sovereign_browser/plans/cross-project-agent-sync.md`](../sovereign_browser/plans/cross-project-agent-sync.md:1)).

The config shape:

```json
{
  "provider": "ppq",
  "api_key": "sk-...",
  "model": "claude-haiku-4.5",
  "base_url": "https://api.ppq.ai",
  "max_tokens": 4096,
  "temperature": 0.7
}
```

### LLM Call

Uses `fetch()` to the OpenAI-compatible `/v1/chat/completions` endpoint. The
existing [`ai-chat.mjs`](../client/www/js/ai-chat.mjs:1) and
[`ai-ui.mjs`](../client/www/js/ai-ui.mjs:72) modules provide reusable
`sendAiChatJson()` and config normalization functions.

### Tool-Call Loop

```javascript
async function agentLoop(messages, toolsSchema, maxTurns) {
    for (let turn = 0; turn < maxTurns; turn++) {
        const response = await callLLM({ messages, tools: toolsSchema });
        if (response.tool_calls.length === 0) {
            return response.content;  // final answer
        }
        messages.push({ role: 'assistant', tool_calls: response.tool_calls });
        for (const tc of response.tool_calls) {
            const result = await toolsExecute(tc.name, tc.arguments);
            messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
        }
    }
    return null;  // max turns exhausted
}
```

### LLM Fallback Chain

Skill `llm` tags use the CSS font-stack style fallback chain
(`anthropic/claude-sonnet, openai/gpt-4o-mini, cheap`). The browser agent
resolves this against its configured providers, matching the C agent's
[`docs/SKILLS.md`](../docs/SKILLS.md:269) behavior.

---

## Nostr Connectivity

### Relay Pool

Two options:

1. **NDK Worker** — reuse [`ndk-worker.js`](../client/www/ndk-worker.js:1) from
   the client project. Provides a Web Worker with relay pool, subscription
   management, event dedup, and NIP-07 signer bridge. This is the fastest path
   — it is already battle-tested in `ai.html` and other client pages.

2. **nostr-tools** — lighter weight, direct in-page. Simpler but less
   infrastructure.

**Recommendation: NDK Worker.** It already handles relay connection management,
reconnection, event dedup, and signer integration. The browser Didactyl page
would import [`init-ndk.mjs`](../client/www/js/init-ndk.mjs:1) for the auth +
worker bootstrap, then use the worker's `subscribe` and `publish` message types.

### Subscriptions on Boot

| Subscription | Filter | Purpose |
|---|---|---|
| Relay list | kind 10002, own pubkey | Discover agent's configured relays |
| Adoption list | kind 10123, own pubkey | Load adopted skills |
| Self skills | kinds 31123, 31124, own pubkey | Load skill definitions |
| Memory | kind 30078, own pubkey, d=memory | Agent long-term memory |
| Tasks | kind 30078, own pubkey, d=tasks | Agent task list |
| LLM config | kind 30078, own pubkey, d=llm_config | Provider/model config |
| DMs | kind 4, own pubkey as p-tag | Admin command channel |
| Admin context | kinds 0, 3, 10002, 1, admin pubkey | WoT + admin awareness |

### Event Dedup

The C agent uses an event-ID cache + FNV-1a fingerprint debounce window. The
browser agent uses a `Set<string>` of seen event IDs with a periodic cleanup
(evict entries older than N minutes). NDK also has built-in dedup.

---

## Identity & Signing

### nostr-login-lite — Primary Auth Path

The page uses [`nostr-login-lite`](../nostr_login_lite/README.md:1) for
authentication and signing. This is the same login library used across the
client project. It provides a NIP-07 compliant `window.nostr` facade that works
with multiple signing methods:

| Method | How It Works | Use Case |
|---|---|---|
| **local** | User enters agent nsec; key is AES-GCM encrypted with a session password and stored in localStorage | Primary — sign in as the agent with its private key |
| **extension** | Delegates to a real NIP-07 browser extension (Alby, nos2x, Amber) | If the agent key is in an extension |
| **connect** | NIP-46 remote signer (bunker) | Agent key held by a remote signer service |
| **nsigner** | USB hardware signer via WebUSB/WebSerial | Agent key on n_signer hardware device |
| **seedphrase** | BIP-39 seed phrase → derive nsec | Agent key from a 12-word seed |
| **readonly** | npub only, no signing | Inspection mode — read relay data, no actions |

The key insight: **you sign in as the agent, with the agent's private key.**
nostr-login-lite installs `window.nostr` with the full NIP-07 surface:

- `window.nostr.getPublicKey()` — returns agent pubkey hex
- `window.nostr.signEvent(event)` — signs any Nostr event with the agent key
- `window.nostr.nip04.encrypt(pubkey, plaintext)` — NIP-04 encrypt
- `window.nostr.nip04.decrypt(pubkey, ciphertext)` — NIP-04 decrypt
- `window.nostr.nip44.encrypt(pubkey, plaintext)` — NIP-44 encrypt (if supported)
- `window.nostr.nip44.decrypt(pubkey, ciphertext)` — NIP-44 decrypt (if supported)

All tool implementations that need to sign events, encrypt DMs, or decrypt
memory route through `window.nostr` — exactly like the C agent routes through
its signer. The agent's nsec never touches the agent code directly; it stays
inside nostr-login-lite's encrypted store or the extension/hardware signer.

### Initialization

```javascript
await window.NOSTR_LOGIN_LITE.init({
    persistence: true,           // Remember login across page reloads
    isolateSession: false,       // Share across tabs (agent is one identity)
    relays: ['wss://relay.damus.io', 'wss://nos.lol'],
    methods: {
        extension: true,         // Browser extension if available
        local: true,             // Manual nsec entry — primary for agent
        readonly: true,          // Inspection mode
        seedphrase: true,        // BIP-39 seed
        connect: true,           // NIP-46 remote signer
        nsigner: true,           // USB hardware signer
    },
    floatingTab: {
        enabled: true,
        appearance: { text: 'Agent Login' }
    }
});
```

After init, nostr-login-lite either restores a persisted session or shows the
login modal. Once authenticated, `window.nostr` is installed and the boot
sequence continues.

### Admin Identity

The admin pubkey is stored in the agent's kind 30078 `d:llm_config` or a
dedicated config event. On boot, the page fetches this to determine the admin
identity for DM classification.

---

## File Layout

The page lives in the client project's `www/` directory alongside the existing
pages, reusing the shared CSS, NDK worker, and JS modules.

```
~/lt/client/www/
├── didactyl-agent.html          # NEW — the browser Didactyl page
├── css/
│   └── client.css               # existing shared styles
├── js/
│   ├── init-ndk.mjs             # existing — NDK worker bootstrap + auth
│   ├── ai-ui.mjs                # existing — LLM config + chat completions URL
│   ├── ai-chat.mjs              # existing — sendAiChatJson()
│   ├── didactyl-agent.mjs       # NEW — agent core: boot, trigger dispatch, loop
│   ├── didactyl-context.mjs     # NEW — two-layer context assembly
│   ├── didactyl-tools.mjs       # NEW — browser-safe tool dispatch + schemas
│   ├── didactyl-skills.mjs      # NEW — skill cache, adoption list, trigger matching
│   ├── didactyl-memory.mjs      # NEW — memory/task persistence via kind 30078
│   └── didactyl-triggers.mjs    # NEW — DM, cron, subscription trigger management
├── ndk-worker.js                # existing — relay pool worker
├── nostr-login-lite/            # existing — login library (vendored or built)
└── template.html                # existing — page template (auth, sidenav, theme)
```

### Module Responsibilities

| Module | Responsibility |
|---|---|
| [`didactyl-agent.mjs`](../client/www/js/didactyl-agent.mjs:1) | Boot sequence, agent loop, message dispatch, UI binding |
| [`didactyl-context.mjs`](../client/www/js/didactyl-context.mjs:1) | Two-layer context assembly, heading bump, role split, template variable resolution |
| [`didactyl-tools.mjs`](../client/www/js/didactyl-tools.mjs:1) | Tool schema definitions, tool dispatch, browser-safe tool implementations |
| [`didactyl-skills.mjs`](../client/www/js/didactyl-skills.mjs:1) | Skill cache, adoption list management, trigger matching |
| [`didactyl-memory.mjs`](../client/www/js/didactyl-memory.mjs:1) | Memory save/recall, task management via kind 30078 |
| [`didactyl-triggers.mjs`](../client/www/js/didactyl-triggers.mjs:1) | DM listener, cron scheduler, subscription trigger registration |

---

## UI Design

### Layout

```
+------------------------------------------------------------------+
|  DIDACTYL AGENT                                    [dark/light]  |
+------------------------------------------------------------------+
|  Status: Connected to 3 relays · Listening for DMs and triggers  |
+------------------------------------------------------------------+
|                                    |                              |
|  +------------------------------+  | +---------------------------+|
|  | CONVERSATION / LOG            |  | | AGENT STATE              ||
|  |                               |  | |                           ||
|  | [admin] Fix the typo in my    |  | | Pubkey: npub1...         ||
|  |       latest post             |  | | Admin:  npub1...         ||
|  |                               |  | | Relays: 3/3 connected    ||
|  | [agent] I found your post     |  | | Skills: 12 adopted       ||
|  |         note1abc...           |  | | Triggers: 3 active       ||
|  |         The typo is "recieve" |  | |                           ||
|  |         → fixing now...       |  | | --- Trigger Log ---      ||
|  |                               |  | | [cron] readme-monitor    ||
|  | [tool] nostr_query → 1 event  |  | | [dm] admin message       ||
|  | [tool] nostr_post → published |  | | [sub] new follower       ||
|  |                               |  | |                           ||
|  | [admin] Thanks!               |  | | --- Skills ---           ||
|  |                               |  | | [x] personality          ||
|  | [agent] Done!                 |  | | [x] chat                 ||
|  |                               |  | | [x] readme-monitor       ||
|  |                               |  | | [ ] spellcheck           ||
|  +------------------------------+  | +---------------------------+|
|                                    |                              |
|  +------------------------------+  | +---------------------------+|
|  | [Message input...]    [Send]  |  | | [Settings]               ||
|  +------------------------------+  | +---------------------------+|
+------------------------------------------------------------------+
```

### Key UI Elements

- **Conversation log** — DM history with the admin, showing tool calls inline
- **Agent state panel** — pubkey, admin, relay status, skill count, trigger status
- **Trigger log** — recent trigger fires with type and skill name
- **Skills list** — adopted skills with toggle (adopts/removes from 10123)
- **Message input** — send a DM to the agent directly (for testing)
- **Settings** — LLM provider config, relay list, admin pubkey

### Reusable Components

The page builds on [`template.html`](../client/www/template.html:1) from the
client project, inheriting:

- Auth flow (NIP-07 login via `init-ndk.mjs`)
- Sidenav navigation
- Theme toggle (dark/light)
- Relay status indicator
- Hamburger morphing menu

---

## Implementation Phases

### Phase 1 — Minimum Viable Agent

Goal: A browser page that boots from a Nostr identity, loads its config and
skills, listens for admin DMs, reasons with an LLM, and replies via DM.

- [ ] Create `didactyl-agent.html` page shell using `template.html` as base
- [ ] Integrate `nostr-login-lite` for auth: init with local/extension/connect/nsigner/seedphrase methods, sign in as agent with its nsec
- [ ] Implement `didactyl-agent.mjs` boot sequence: nostr-login-lite auth → `window.nostr.getPublicKey()` → relay connect → subscribe to config/skills/adoption/DMs
- [ ] Implement `didactyl-skills.mjs`: skill cache, adoption list parsing, trigger matching for `dm` trigger type
- [ ] Implement `didactyl-context.mjs`: two-layer context assembly, heading bump, role split, `{{message}}` and `{{skill_d_tag}}` variable resolution
- [ ] Implement `didactyl-tools.mjs`: core Nostr tools (`nostr_post`, `nostr_query`, `nostr_dm_send`, `nostr_profile_get`, `nostr_pubkey`, `nostr_npub`, `tool_list`), OpenAI-format tool schemas
- [ ] Implement `didactyl-memory.mjs`: `memory_save`, `memory_recall`, `task_manage`, `task_list` via kind 30078
- [ ] Implement agent loop: build context → call LLM → execute tools → loop → reply as DM
- [ ] Implement sender tier classification (admin / WoT / stranger)
- [ ] Wire up conversation log UI + agent state panel
- [ ] LLM config from kind 30078 `d:llm_config` with localStorage fallback

### Phase 2 — Full Tool Set + Triggers

Goal: Complete the browser-safe tool set and add cron + subscription triggers.

- [ ] Add remaining Nostr tools: `nostr_delete`, `nostr_react`, `nostr_dm_send_nip17`, `nostr_encrypt`, `nostr_decrypt`, `nostr_nip05_lookup`, `nostr_encode`, `nostr_decode`
- [ ] Add skill tools: `skill_create`, `skill_edit`, `skill_list`, `skill_adopt`, `skill_remove`, `skill_search`
- [ ] Add config/model tools: `model_get`, `model_set`, `config_store`, `config_recall`, `agent_version`
- [ ] Add Blossom tools: `blossom_upload`, `blossom_download`, `blossom_head`, `blossom_list`, `blossom_delete`
- [ ] Add browser-native tools: `browser_fetch`, `browser_tab_open`, `browser_clipboard`, `browser_local_storage`
- [ ] Implement `didactyl-triggers.mjs`: cron trigger scheduler, nostr-subscription trigger registration, chain trigger
- [ ] Add trigger log UI
- [ ] Add skills management UI (adopt/remove, create/edit)

### Phase 3 — Advanced

Goal: Push the boundaries of what a browser agent can do.

- [ ] Cashu wallet tools (NIP-60) — all HTTP-based, feasible in browser
- [ ] File System Access API — `local_file_read` / `local_file_write` for user-selected directories
- [ ] Service Worker — keep cron triggers alive when tab is backgrounded
- [ ] NIP-17 gift-wrap messaging (requires nip44 signer support)
- [ ] Cross-project settings sync — read/write shared `d:user-settings` kind 30078
- [ ] Broadcast-debounce admin client — fan-out DMs to multiple agents (browser + C), accept first reply (from [`DECENTRALIZED_DIDACTYL.md`](DECENTRALIZED_DIDACTYL.md:1))
- [ ] Mobile responsive layout
- [ ] Offline skill caching via IndexedDB

---

## Compatibility with C Didactyl

### Shared State

The browser agent and C agent share **all state via Nostr events** — no direct
communication is needed. Both read and write the same event kinds:

| State | Event | Shared |
|---|---|---|
| Skills | kind 31123/31124 | Yes — both publish and consume |
| Adoption list | kind 10123 | Yes |
| Memory | kind 30078 d=memory | Yes (encrypted, same key needed) |
| Tasks | kind 30078 d=tasks | Yes |
| LLM config | kind 30078 d=llm_config | Yes |
| DMs | kind 4 | Yes |
| Profile | kind 0 | Yes |

### Same Keys, Different Runtimes

If the browser agent and C agent use the **same nsec**, they share a Nostr
identity. This has the same problems described in
[`DECENTRALIZED_DIDACTYL.md`](DECENTRALIZED_DIDACTYL.md:1): both subscribe to
the same DMs, both process, both reply. The broadcast-debounce model applies.

**Recommended: different keys.** The browser agent is a distinct agent with its
own identity. The admin talks to whichever agent is appropriate. Skills and
soul can be shared via adoption lists.

### Skill Portability

Skills are portable by design. A skill published by the C agent works in the
browser agent and vice versa, as long as:

1. The skill's `tools` tag only references browser-available tools
2. The skill doesn't depend on shell-specific capabilities

The `llm` fallback chain ensures model portability across runtimes.

---

## Security Considerations

| Concern | Mitigation |
|---|---|
| nsec in browser memory | nostr-login-lite encrypts the nsec with AES-GCM and a session password before storing in localStorage. For maximum security, use nsigner (hardware) or connect (NIP-46 remote) methods so the key never enters the browser. |
| LLM API key exposure | Key is in kind 30078 encrypted event. Browser decrypts to memory. Never logged. |
| Tool execution safety | Browser sandbox limits damage. No shell, no filesystem (Phase 1/2). |
| DM sender spoofing | Verify event signatures before processing (NDK does this). Classify sender tier. |
| Relay trust | Relays are untrusted. Event signatures verified. Encrypted DMs. |
| CORS for LLM calls | OpenAI-compatible APIs typically support CORS. If not, use a proxy. |
| Blossom auth | Signed auth events via `window.nostr.signEvent`. |

---

## Reusable Infrastructure

| Asset | Location | What It Provides |
|---|---|---|
| NDK Worker | [`ndk-worker.js`](../client/www/ndk-worker.js:1) | Relay pool, subscriptions, event dedup, publish |
| NDK Init | [`init-ndk.mjs`](../client/www/js/init-ndk.mjs:1) | NIP-07 auth, worker bootstrap, mute list |
| nostr-login-lite | [`nostr-login-lite`](../nostr_login_lite/README.md:1) | NIP-07 facade: local nsec, extension, NIP-46, nsigner, seedphrase, readonly |
| LLM Config | [`ai-ui.mjs`](../client/www/js/ai-ui.mjs:1) | Config normalization, chat completions URL, provider merge |
| LLM Call | [`ai-chat.mjs`](../client/www/js/ai-chat.mjs:1) | `sendAiChatJson()` — fetch-based LLM call |
| Page Template | [`template.html`](../client/www/template.html:1) | Auth, sidenav, theme toggle, relay status |
| Shared CSS | [`client.css`](../client/www/css/client.css:1) | Theme variables, layout, components |
| Skills Demo | [`skills-demo.mjs`](../client/www/js/skills-demo.mjs:1) | Skill discovery, template parsing, execution |

---

## Open Questions

1. **Should the browser agent use the same relay list as the C agent (from kind
   10002), or its own?** Same is simpler and more sovereign. Different allows
   geographic diversity.

2. **NIP-17 gift-wrap support depends on the signer having nip44.** Not all
   NIP-07 extensions support this. Fallback to NIP-04 or skip NIP-17 tools if
   unavailable.

3. **Cron triggers die when the tab closes.** Is this acceptable, or do we need
   a Service Worker? For a true "always-on" agent, the C binary is still the
   right choice. The browser agent is for interactive/attended use.

4. **Should we support the broadcast-debounce admin client pattern from
   [`DECENTRALIZED_DIDACTYL.md`](DECENTRALIZED_DIDACTYL.md:1) so the admin can
   fan-out to both browser and C agents?** This is a Phase 3 enhancement.
