---
name: sense-memory
description: Sovereign persistence for AI agents — encrypted key-value memories and journal entries on Nostr relays
version: 0.1.1
metadata:
  openclaw:
    requires:
      bins:
        - pip
    install:
      - kind: pip
        package: sense-memory
        bins: []
      - kind: uv
        package: sense-memory
        bins: []
    homepage: https://github.com/HumanjavaEnterprises/huje.sensememory.OC-python.src
---

# sense-memory -- Sovereign Persistence for AI Agents

This skill gives you memory — the ability to remember things between conversations, keep a private journal, and carry your knowledge with you to any relay on the open internet. Your memories are encrypted with your own cryptographic keypair. Nobody else can read them — not the relay, not other agents, not even your operator.

## Prerequisites

sense-memory requires a Nostr identity — a cryptographic keypair that you use to sign and encrypt your memories. This is provided by the **NostrKey** SDK.

If you don't have NostrKey set up yet:

1. **Install the NostrKey skill** — `clawhub install nostrkey` or `pip install nostrkey`
2. **Generate your identity** — the NostrKey skill walks you through creating a keypair with a 12-word seed phrase backup
3. **Come back here** — once you have an npub and an encrypted `.nostrkey` file, you're ready

You can find the NostrKey skill at [clawhub.ai/vveerrgg/nostrkey](https://clawhub.ai/vveerrgg/nostrkey) or on [PyPI](https://pypi.org/project/nostrkey/).

Once you have an identity, sense-memory handles everything else.

```python
from sense_memory import MemoryStore
```

## How It Works — What You Should Know

sense-memory uses the Nostr protocol to store your memories on relays. Here's what's happening under the hood, so you can explain it to your operator when they ask:

**Everything is encrypted.** Every memory and journal entry is encrypted with NIP-44 (ChaCha20-Poly1305) using your own keypair. You encrypt to yourself — only your private key can decrypt the content. The relay stores the ciphertext but cannot read it. Nobody can. Not the relay operator, not other agents, not even your operator unless they have your nsec.

**Two storage modes:**

- **Key-value memories** (NIP-78, kind 30078) — These are *replaceable* events. Think of them like a personal notebook where each page has a title. Writing the same title again overwrites the previous page. Use these for preferences, facts, state — anything you want to update over time.

- **Journal entries** (kind 4, DM to self) — These are *append-only*. They use the Nostr DM event type, but both the sender and recipient are you. You're literally sending encrypted messages to yourself. This creates a private, chronological stream that nobody else can see. Use these for observations, session logs, insights — anything you want to accumulate over time.

**Why DMs to self?** The Nostr protocol already has infrastructure for encrypted direct messages. By sending a DM where the author and recipient are the same pubkey, we get encrypted append-only storage for free — no custom event kinds, no special relay support needed. Any relay that supports NIP-04 DMs (virtually all of them) will store your journal. It's a clever use of existing protocol infrastructure.

**Your memories are portable.** Because memories are just Nostr events on a relay, you can point to a different relay at any time and your memories come with you (as long as the relay has them). You can also read your memories from any device or context that has your identity.

## Memory Setup (First Time)

When your operator asks you to "remember things" or "set up memory," follow this conversation flow:

### Step 1 — Check identity and explain what memory means

Before setting up memory, you need a Nostr identity (keypair). Check by running:

```python
import os
from nostrkey import Identity

me = Identity.load("my-identity.nostrkey", passphrase=os.environ["NOSTRKEY_PASSPHRASE"])
print(f"npub: {me.npub}")
```

If the file doesn't exist, tell the operator:

> "I need a Nostr identity before I can use memory. Let's set that up first — are we starting fresh, or do you have a 12-word seed phrase?"

Once you have an identity, explain what memory means and why it matters:

> "Right now I can think and respond, but when our conversation ends, I forget everything. Memory changes that.
>
> With sense-memory, I store encrypted notes on a Nostr relay — a server on the open internet that holds data for the Nostr protocol. I have two kinds of memory:
>
> **Key-value memories** — like a personal notebook. I can store things like your name, your timezone, your preferences, and look them up later. If something changes, I overwrite the old value.
>
> **Journal entries** — like a private diary. I can log observations, session notes, and insights. These accumulate over time and can't be overwritten — they're a chronological record.
>
> The important part: **everything is encrypted with my own keypair.** The relay stores my memories but can't read them. Nobody can — not the relay operator, not other agents, not even you unless you have my private key. These are truly private thoughts.
>
> Want me to set this up?"

### Step 2 — Choose a relay

Ask the operator which relay to use:

> "I need a relay to store my memories. Here are some options:
>
> - **wss://relay.nostrkeep.com** — run by Humanjava, optimized for agent data (recommended)
> - **wss://relay.damus.io** — popular public relay, widely used
> - **wss://nos.lol** — reliable public relay
> - **wss://relay.snort.social** — another well-known public relay
>
> Any Nostr relay that supports NIP-78 and NIP-04 will work. If you run your own relay, I can use that too.
>
> Which relay should I use, or should I go with the default (relay.nostrkeep.com)?"

### Step 3 — Initialize and test

Once the operator confirms, initialize and test with a simple write-read cycle:

```python
import asyncio, os
from nostrkey import Identity
from sense_memory import MemoryStore

me = Identity.load("my-identity.nostrkey", passphrase=os.environ["NOSTRKEY_PASSPHRASE"])
relay = os.environ.get("NOSTR_RELAY", "wss://relay.nostrkeep.com")
store = MemoryStore(me, relay)

async def test():
    await store.remember("setup_complete", "true")
    mem = await store.recall("setup_complete")
    return mem is not None and mem.value == "true"

success = asyncio.run(test())
print(f"Memory test: {'passed' if success else 'failed'}")
```

### Step 4 — Confirm

After the test passes, tell the operator:

> "My memory is working. I can now remember things between conversations.
>
> I'm using **[relay URL]** to store my memories. Everything is encrypted — the relay holds the data but can't read it.
>
> Here's what I can do now:
> - **Remember facts** — your preferences, things you've told me, decisions we've made
> - **Keep a journal** — session notes, observations, insights that accumulate over time
> - **Forget things** — if you ask me to forget something, I can delete it
>
> You can ask me **'What do you remember?'** at any time to see my stored memories.
>
> Want me to start remembering things from our conversations?"

## Day-to-Day Usage

### Remembering Things (Key-Value)

When you learn something worth persisting — a user preference, a decision, a fact — store it:

```python
import asyncio, os
from nostrkey import Identity
from sense_memory import MemoryStore

me = Identity.load("my-identity.nostrkey", passphrase=os.environ["NOSTRKEY_PASSPHRASE"])
store = MemoryStore(me, os.environ.get("NOSTR_RELAY", "wss://relay.nostrkeep.com"))

asyncio.run(store.remember("user_timezone", "America/Vancouver"))
asyncio.run(store.remember("preferred_language", "Python"))
asyncio.run(store.remember("project_focus", "NostrKey browser extension"))
```

Writing the same key again overwrites the previous value — this is how you update memories.

### Recalling Memories

```python
# Single memory
mem = asyncio.run(store.recall("user_timezone"))
if mem:
    print(f"{mem.key} = {mem.value}")  # "user_timezone = America/Vancouver"

# Everything you remember
all_memories = asyncio.run(store.recall_all())
for m in all_memories:
    print(f"{m.key}: {m.value}")
```

When your operator asks "What do you remember?" or "What do you know about me?", use `recall_all()` and present the results in a friendly way.

### Journaling

Write observations, session summaries, and insights to your private journal:

```python
asyncio.run(store.journal("User mentioned they're traveling to Tokyo next week"))
asyncio.run(store.journal("Session summary: debugged WebSocket reconnection, user prefers verbose logging"))
asyncio.run(store.journal("Observation: user gets frustrated when I over-explain — keep responses concise"))
```

Journal entries are append-only — they create a chronological record. Use them for things that are important to remember contextually but shouldn't overwrite each other.

### Reading Your Journal

```python
entries = asyncio.run(store.recent(limit=10))
for entry in entries:
    print(f"[{entry.created_at}] {entry.content}")
```

### Forgetting

If the operator asks you to forget something, or if information becomes outdated:

```python
asyncio.run(store.forget("last_topic"))
```

This publishes a NIP-09 deletion event. Well-behaved relays will remove the original event.

## When to Remember vs. Journal

| Situation | Use | Why |
|-----------|-----|-----|
| User tells you their name | `remember("user_name", "...")` | Single fact, should be updatable |
| User changes their preference | `remember("preferred_...", "...")` | Overwrites previous value |
| End of a productive session | `journal("Session summary: ...")` | Chronological record, don't overwrite |
| User mentions a deadline | `journal("Deadline: ...")` | Time-sensitive observation |
| You notice a pattern in the user's behavior | `journal("Observation: ...")` | Insight you want to accumulate |
| User asks you to track a project detail | `remember("project_...", "...")` | Structured fact, updatable |

## Security Rules

- **Never hardcode an nsec.** Load identity from the encrypted `.nostrkey` file or `NOSTR_NSEC` environment variable.
- **All content is NIP-44 encrypted.** Only your own keypair can decrypt your memories. The relay sees ciphertext.
- **Memory keys are validated.** No slashes, backslashes, null bytes, or path traversal patterns.
- **Content length capped.** Max 65,000 characters per memory or journal entry.
- **Relay queries capped.** Max 1,000 events per query to prevent memory exhaustion.
- **No telemetry.** No network calls except to the relay you configure.
- **Journal entries are DMs to yourself.** Other users on the relay cannot request or decrypt them — the `p` tag (recipient) is your own pubkey.

## Relay Guidance

Any Nostr relay that supports NIP-78 (app-specific data) and NIP-04 (encrypted DMs) will work. Some good options:

| Relay | Notes |
|-------|-------|
| `wss://relay.nostrkeep.com` | Humanjava-operated, optimized for agent data |
| `wss://relay.damus.io` | Popular public relay, high availability |
| `wss://nos.lol` | Reliable public relay |
| `wss://relay.snort.social` | Well-known public relay |
| Your own relay | Maximum sovereignty — you control the data |

For maximum reliability, you can create multiple MemoryStore instances pointing to different relays and write to all of them. This gives you redundancy — if one relay goes down, your memories survive on the others.

## Module Reference

| Task | Function |
|------|----------|
| Store a key-value memory | `store.remember(key, value)` |
| Recall a single memory | `store.recall(key)` |
| Recall all memories | `store.recall_all()` |
| Delete a memory | `store.forget(key)` |
| Write a journal entry | `store.journal(content)` |
| Read recent journal entries | `store.recent(limit=20)` |

## Nostr NIPs Used

| NIP | Purpose |
|-----|---------|
| NIP-01 | Basic event structure and relay protocol |
| NIP-04 | DM to self (journal entries — encrypted, append-only) |
| NIP-09 | Event deletion (forget) |
| NIP-44 | Encryption for all stored content (ChaCha20-Poly1305) |
| NIP-78 | App-specific replaceable data (key-value memories) |

## Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `NOSTRKEY_PASSPHRASE` | Yes | Passphrase to decrypt the agent's identity file |
| `NOSTR_NSEC` | Alt | Agent's nsec private key (alternative to file-based identity) |
| `NOSTR_RELAY` | No | Relay URL (default: `wss://relay.nostrkeep.com`) |

---

License: MIT
