# Decentralized Didactyl

## The Question

Can you run Didactyl on multiple servers to gain censorship resistance, geographic distribution, and high availability?

---

## Same Keys vs. Different Keys

### Same Private Key on Multiple Servers

Running two or more Didactyl instances with the **same nsec** means they share a Nostr identity — same pubkey, same signature authority. This breaks almost immediately.

The core problem is **state conflicts on replaceable events**. Didactyl's identity is built on replaceable Nostr event kinds — kind `10002` (relay list), kind `10123` (skill adoption list), kind `31120` (soul), kind `31123`/`31124` (skills), kind `0` (profile). Replaceable events use `created_at` timestamps where the latest event wins. Two instances publishing the same replaceable kind within seconds creates a race where relays disagree on which version is canonical.

Beyond relay-level conflicts, each Didactyl process maintains significant **in-memory state** that is never shared: DM dedup caches, message fingerprint ring buffers, trigger cooldown timers, conversation history, and the self-skill cache. Two instances with the same key both subscribe to `#p` = their shared pubkey. The same admin DM arrives at both. Both process it. Both call the LLM. Both reply. The admin gets two possibly contradictory responses.

Making same-key work would require a coordination layer — leader election, distributed locks, or external shared state — all of which add complexity and defeat the decentralization goal.

### Different Private Keys on Multiple Servers

Each instance is a **distinct agent** with its own Nostr identity. No state conflicts, no replaceable-event races, no shared in-memory caches to synchronize. Each agent independently subscribes to relays, processes DMs, calls its LLM, and replies.

This is architecturally clean and aligned with how Nostr works. The question becomes: how do multiple independent agents cooperate to serve a single admin?

---

## How We Arrived at the Broadcast-Debounce Model

The first instinct with different keys is **delegation** — a primary agent receives the admin's DM and farms out subtasks to specialist agents. But this adds a single point of failure (the primary), requires an agent-to-agent trust protocol, and means the admin is still talking to one agent.

A simpler model: **broadcast the same message to all agents, let them all respond independently, and debounce on the admin side.** The admin sends the same DM to N agent pubkeys. Each agent processes it through its own LLM, its own tools, its own context. The admin collects replies and accepts the first one (or the best one). The others are discarded.

This requires no coordination protocol between agents. No leader election. No shared state. No new Nostr event kinds. The agents don't even need to know about each other. All complexity lives at the edge — in the admin's client.

---

## The Broadcast-Debounce Architecture

```mermaid
flowchart TD
    ADMIN[Admin Client<br/>with fan-out + debounce]
    
    ADMIN -->|"DM: task"| A[Agent A<br/>Server 1 — Iceland<br/>anthropic/claude-sonnet]
    ADMIN -->|"DM: task"| B[Agent B<br/>Server 2 — Brazil<br/>openai/gpt-4o]
    ADMIN -->|"DM: task"| C[Agent C<br/>Server 3 — Singapore<br/>local Ollama]
    
    A -->|"DM reply"| ADMIN
    B -->|"DM reply"| ADMIN
    C -->|"DM reply"| ADMIN
    
    ADMIN -->|"Accept first reply,<br/>discard rest"| RESULT[Response shown to admin]
```

### What Each Agent Needs

Each agent runs standard Didactyl with its own config:

- **Own nsec/npub** — distinct Nostr identity
- **Same admin pubkey** — all agents recognize the same admin
- **Same soul** — published as kind `31120` from each agent's key, or adopted from a shared author
- **Same adopted skills** — each agent's kind `10123` references the same skill events
- **Own LLM config** — can be different providers for diversity
- **Own relay list** — can overlap or be completely different for geographic distribution

No code changes to Didactyl are required. Each agent is a standard, unmodified instance.

### What the Admin Client Needs

The admin side needs a thin coordination layer:

1. **Fan-out** — when the admin sends a message, the client sends it as a DM to all N agent pubkeys
2. **Collection** — the client listens for DM replies from all agent pubkeys
3. **Debounce** — the client tags each outgoing message with a task identifier (hash of message + nonce). The first reply that corresponds to a pending task is accepted. Subsequent replies for the same task are logged but suppressed from display.

This could be implemented as:
- A modified Nostr client
- A thin proxy between the admin and the agents
- A Didactyl agent whose sole job is fan-out and collection

### Properties

| Property | Benefit |
|----------|---------|
| **No shared state** | Each agent is fully independent — no coordination protocol needed |
| **No code changes** | Works with current Didactyl for read-only tasks |
| **Censorship resistant** | Kill any N-1 servers, the last one still works |
| **Provider diverse** | Different LLMs on each agent — different failure modes, different strengths |
| **Geographically distributed** | Different jurisdictions, different relay access |
| **Graceful degradation** | Losing agents means fewer responses, not failure |
| **Complexity at the edge** | Agents stay simple and sovereign; the admin client handles coordination |

### The Write-Action Problem

For read-only tasks — questions, summaries, drafts — the model works perfectly. All agents respond, admin picks one, no side effects.

For write actions — "post a note about Bitcoin" — all three agents independently publish to Nostr. Three posts appear. The admin wanted one.

**Solution: two-phase draft-then-confirm.** Teach all agents via their soul or an adopted skill to never execute write actions directly. Instead, they draft the action and report what they *would* do. The admin reviews the drafts, picks one, and sends a confirmation DM to that specific agent. The other agents' drafts are simply never confirmed.

The skill instruction is straightforward:

> When asked to publish, post, react, delete, or execute any action with side effects, draft the content and present it for approval. Do not execute until the admin explicitly confirms.

This requires no code changes — only a soul or skill update. It also happens to be good practice for any agent handling important actions.

### Trigger Handling

Triggered skills (cron, nostr-subscription, webhook) present the same duplication issue as write actions — all agents fire the same trigger independently.

Options:
- **Disable triggers on all but one agent** — simple, but creates a single point of failure for triggered tasks
- **Use the draft-then-confirm pattern for triggered actions** — triggers draft and report to admin rather than acting directly
- **Accept duplication for idempotent triggers** — reactions, replaceable event updates, and monitoring alerts are harmless when duplicated
- **Shard triggers across agents** — Agent A handles cron triggers, Agent B handles nostr-subscription triggers, Agent C handles webhooks

### Future: Agent Awareness

In the basic model, agents don't know about each other. A future enhancement could add awareness:

- Each agent publishes a heartbeat event (kind `30078` with a timestamp)
- Other agents (or the admin client) monitor heartbeats to detect which agents are alive
- The admin client adjusts fan-out based on which agents are responsive

This remains fully Nostr-native — heartbeats are just events on relays — and requires no direct agent-to-agent communication.

---

## Beyond Nostr Posts: Software Development as the Task

The broadcast-debounce model above assumes the agents' primary job is Nostr social activity — posting notes, reacting, querying relays. But what if the task is **software development and infrastructure management**? For example: creating and maintaining a website, deployed across servers in different jurisdictions.

This changes the model fundamentally. The agents are no longer just talking — they are **building things on their local machines**.

### What Didactyl Already Has

Each agent already has local system tools:

| Tool | Capability |
|------|-----------|
| `local_shell_exec` | Execute any shell command — git, npm, docker, systemctl, curl |
| `local_file_read` | Read source files from the working directory |
| `local_file_write` | Write/create source files in the working directory |
| `local_http_fetch` | Make HTTP requests — test endpoints, call APIs, download resources |

An agent with these tools can already: clone a repo, edit source code, run a build, start a dev server, deploy to production, check if a site is up, read logs, and fix issues. The LLM reasons about what to do; the tools are the hands.

### The Shift: From Redundancy to Distribution

With Nostr posting, multiple agents doing the same task is **redundancy** — you want one result and discard the rest. With software development, multiple agents doing *different parts* of the same project is **distribution** — you want all of them to contribute.

```mermaid
flowchart TD
    subgraph "Nostr Posting Model"
        direction LR
        N_ADMIN[Admin] -->|"Same task"| N_A[Agent A]
        N_ADMIN -->|"Same task"| N_B[Agent B]
        N_ADMIN -->|"Same task"| N_C[Agent C]
        N_A -->|"Response 1"| N_DEDUP[Debounce:<br/>pick one]
        N_B -->|"Response 2"| N_DEDUP
        N_C -->|"Response 3"| N_DEDUP
    end

    subgraph "Software Dev Model"
        direction LR
        S_ADMIN[Admin] -->|"Build the site"| S_A[Agent A:<br/>Frontend]
        S_ADMIN -->|"Build the site"| S_B[Agent B:<br/>Backend API]
        S_ADMIN -->|"Build the site"| S_C[Agent C:<br/>Infrastructure]
        S_A -->|"React app ready"| S_MERGE[Integration:<br/>combine all parts]
        S_B -->|"API deployed"| S_MERGE
        S_C -->|"Nginx + TLS configured"| S_MERGE
    end
```

### Architecture: Agents as Jurisdiction-Local Builders

Consider a website that needs to exist in multiple jurisdictions for censorship resistance. Each agent lives on a server in a different country and maintains a local copy of the site.

```mermaid
flowchart TD
    ADMIN[Admin<br/>npub1admin...]
    GIT[(Git Repo<br/>shared source of truth)]
    
    subgraph "Iceland"
        A[Agent A<br/>npub1aaa...]
        A_FS[Local filesystem:<br/>/var/www/mysite]
        A_SRV[Nginx serving<br/>mysite.is]
        A --> A_FS --> A_SRV
    end
    
    subgraph "Brazil"
        B[Agent B<br/>npub1bbb...]
        B_FS[Local filesystem:<br/>/var/www/mysite]
        B_SRV[Nginx serving<br/>mysite.br]
        B --> B_FS --> B_SRV
    end
    
    subgraph "Singapore"
        C[Agent C<br/>npub1ccc...]
        C_FS[Local filesystem:<br/>/var/www/mysite]
        C_SRV[Nginx serving<br/>mysite.sg]
        C --> C_FS --> C_SRV
    end
    
    ADMIN -->|"DM: update the homepage"| A
    ADMIN -->|"DM: update the homepage"| B
    ADMIN -->|"DM: update the homepage"| C
    
    A <-->|"git pull / push"| GIT
    B <-->|"git pull / push"| GIT
    C <-->|"git pull / push"| GIT
```

### Git as the Coordination Layer

In the Nostr posting model, the write-action problem was solved with draft-then-confirm. For software development, the natural coordination layer is **git**.

- All agents share a git repository (hosted on Gitea, GitHub, a Nostr-based git relay, or even a bare repo on one of the servers)
- Each agent works on its local clone
- Before making changes, the agent pulls latest
- After making changes, the agent commits and pushes
- Conflicts are resolved by the LLM (or flagged to the admin)

Git gives you:
- **Atomic changes** — commits are all-or-nothing
- **Conflict detection** — if two agents edit the same file, git tells you
- **History** — every change is traceable to which agent made it
- **Rollback** — bad changes can be reverted

The agent already has `local_shell_exec` which can run `git pull`, `git add`, `git commit`, `git push`. No new tools needed.

### Three Operational Modes

Depending on the task, the admin can use different patterns:

#### Mode 1: Broadcast-Identical (Same Task, All Servers)

> "Pull latest and rebuild the site"

All agents do the same thing on their local server. This is the deployment/ops pattern — you want the same action everywhere. No debounce needed; all agents should execute.

#### Mode 2: Broadcast-Debounce (Same Task, One Result)

> "Write a new About page"

All agents draft a version. Admin picks the best one. That agent commits and pushes. The other agents then pull the update in the next sync cycle. This is the creative/development pattern.

#### Mode 3: Directed (Specific Task, Specific Agent)

> To Agent A only: "Debug why the Iceland server returns 502"

The admin sends a DM to one specific agent because the task is server-specific. This is the ops/debugging pattern.

### The Sync Cycle

For the site to stay consistent across jurisdictions, agents need a periodic sync. This can be a **cron-triggered skill**:

```
You are a deployment sync agent. Every 15 minutes:
1. Run `git pull` in the project directory
2. If there are new changes, run the build command
3. Restart the web server if the build succeeded
4. DM the admin a brief status: what changed, build result, server status
5. If the pull fails due to conflicts, DM the admin with the conflict details and do not build
```

This skill already works with Didactyl's existing `cron` trigger type and `local_shell_exec` tool. Each agent runs it independently on its own server.

### What Changes vs. the Nostr Model

| Aspect | Nostr Posting | Software Development |
|--------|--------------|---------------------|
| **Primary tools** | `nostr_post`, `nostr_query` | `local_shell_exec`, `local_file_write`, `local_file_read` |
| **Coordination** | Debounce on admin side | Git repo as shared state |
| **Write conflicts** | Replaceable events (last-write-wins) | Git merge conflicts (explicit resolution) |
| **Broadcast meaning** | Redundancy — pick one response | Deployment — all servers execute |
| **Agent specialization** | All agents are identical | Agents can have server-specific skills |
| **State** | Nostr relays (stateless agents) | Local filesystem (stateful agents) |
| **Failure mode** | Agent down = fewer responses | Agent down = one jurisdiction offline |

### The Stateful Agent Problem

This is the fundamental shift. Nostr-posting agents are essentially stateless — their identity and skills live on relays, and destroying the host doesn't kill the agent. Software development agents are **deeply stateful** — they have local files, running services, build artifacts, and server configurations that don't exist on Nostr.

Git mitigates this: if an agent's server dies, you spin up a new server, install Didactyl with the same agent keys, and `git clone` the repo. The agent recovers its working state from git. But the server-specific state (Nginx config, TLS certs, DNS, running processes) needs to be reconstructable too.

This suggests a **infrastructure-as-code** discipline where everything about the server's configuration is in the git repo:
- Nginx configs
- Docker compose files
- TLS certificate automation (Let's Encrypt)
- Deployment scripts

The agent's first-boot skill becomes: "Clone the repo, run the setup script, verify the site is serving."

### Jurisdiction-Specific Considerations

Each agent can have **server-specific skills** that account for local differences:

| Agent | Jurisdiction | Specific Skills |
|-------|-------------|----------------|
| Agent A | Iceland | Icelandic privacy law compliance, `.is` domain management |
| Agent B | Brazil | LGPD compliance checks, `.br` domain management |
| Agent C | Singapore | PDPA compliance, `.sg` domain management, Asia-Pacific CDN config |

These are adopted as private skills (kind `31124`) specific to each agent, while the core development skills (kind `31123`) are shared across all agents.

### Summary

The broadcast-debounce model extends naturally to software development, but the coordination mechanism shifts from admin-side debounce to **git as shared state**. The three operational modes — broadcast-identical for deployment, broadcast-debounce for development, and directed for debugging — cover the full range of tasks. Didactyl's existing tools (`local_shell_exec`, `local_file_read`, `local_file_write`) and trigger types (`cron` for sync cycles) already support this without code changes. The main new requirement is disciplined infrastructure-as-code so that agent state is recoverable from git.
