docs(nips): NIP-AE — Agent Engrams#575
Merged
Merged
Conversation
Defines a convention for AI agents to store persistent encrypted memory on Nostr as addressable kind:30078 events. The agent owns and signs every record; the owner can always decrypt (NIP-44 conversation key is symmetric) but cannot write. Highlights: - Slug-derived opaque d-tags via HMAC(K_c, ...) — no slug leakage in tags. - Two record types (core, memory) sharing one envelope, demuxed by body shape. - Monotonic created_at write rule defeats same-second NIP-01 tiebreaks. - Wiki-link [[slug]] reachability graph; orphan-aware listing. - Test vectors pinned end-to-end (keys, K_c, d-tags, ciphertexts, ids, sigs) with implementation gotchas (raw shared_x ECDH, BIP-340 aux=zeros, NIP-01 serialization). Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Codex head-to-head review against NIP-44 flagged silent-divergence surface in two places; both are real and worth fixing: 1. Configured relays: ordered precedence (NIP-65 first, OOB fallback) plus an explicit URL canonicalization rule (case-fold scheme/host, strip default port, strip trailing slash on empty path) so two parties compute the same set from the same input. 2. Write verification: pin the gating event (first relay OK) instead of a free-form implementation timeout, and add a one-second propagation default to absorb relay-set skew. Conflict surfacing semantics are unchanged. Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Earlier NIP-AE drafts rode on kind:30078 (NIP-78 application-specific data). A dedicated addressable kind cleans this up: - One less coordination concern (no need for the validity-gate-as-namespace- demultiplexer wording; the kind itself segregates). - Predictable address space for relay implementers. - Honest claim on a slot in the NIP-01 addressable range (30000-39999). Choice of 30174 is mnemonic: 30 || 0xAE where AE is Agent Engrams — the same hex encoding the NIP indicator already uses. All test vectors regenerated under the new kind. The conversation key, d-tag derivations, and ciphertexts for events 1-3 are unchanged (none of those derivations include kind). Event 4's body embeds event 2's id, so event 4's ciphertext also moves; event ids and signatures for all four events change because kind is in the NIP-01 id pre-image. Self-verification preserved: all four event ids re-derive from the spec's own inputs and pubkey_a/pubkey_o/K_c/d-values are unchanged from prior revisions. Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
3b6dd1e to
85fcb6a
Compare
Five passes of codex review against the upstream NIPs surfaced a mix of
real structural issues and rubric pressure for further simplification.
Folding the substantive ones in one commit:
- Drop `core.index` entirely. The walk in *Listing* is authoritative and
was already the recovery path for any divergence; making the cache
in-band created a conflict surface (concurrent writers can race the
index update separately from the memory write) for no protocol-level
benefit. Clients MAY keep an out-of-band `{slug -> {event_id,
created_at}}` cache; that's now noted as implementation-local.
Drops `index` from the core body, the *Reindex* section, and the
"core write follows every memory write" guidance. Event 4's body
and therefore its content, id, and sig are regenerated.
- `p` tag stops fighting NIP-65. Previous text told clients NOT to
fan out memory events to the owner's read relays. The carve-out is
worse than the deviation: now the spec just notes that authoritative
discovery is from the agent's configured relays, and copies arriving
via NIP-65 fanout on owner read relays are a redundant cache, not a
separate channel. Metadata leak is already in Security.
- URL canonicalization is for comparison only. Implementations connect
to the advertised form so relay-side path/host disambiguation is
preserved.
- Write-verify softened from MUST to SHOULD. The one-second propagation
window and "MUST NOT silently retry" are policy, not protocol;
surfaced as best-effort with conflict semantics still defined.
- Clock-poison check softened: keep the monotonicity rule, drop the
fixed 600s threshold; treat clock-poisoning as SHOULD-detect with
implementation-chosen threshold.
- `core` no longer SHOULD-updates on every memory write (consequence
of dropping `index`).
- NIP-09 paragraph: explicitly states only `pubkey_a` may author
deletions (per NIP-09 author constraint), and "deletes all versions"
softened to "requests deletion of" (relay policy, not protocol
guarantee).
- Dedicated kind justification added: namespace isolation from any
other app sharing the agent's pubkey, plus identification by kind
alone without NIP-44 decryption as a demultiplexer.
- *Listing* explicit about best-effort: Nostr has no protocol-level
pagination, so any complete-set claim is provisional; clients
SHOULD surface per-relay limit signals where available.
- Relay-set migration: one-sentence MAY note that agents republish
current heads to a new set before decommissioning a relay they are
leaving; this NIP defines no automatic migration mechanism.
- `nsec_a`/`nsec_o` renamed to `seckey_a`/`seckey_o` in vectors and
Security (nsec is the NIP-19 bech32 form; the inputs are raw hex
scalars).
- NIP-31 `alt` tag noted as MAY (non-leaking summary for unknown-kind
viewers).
- Security: agent-key compromise note now mentions that the holder
can also re-derive K_c and break confidentiality, not just integrity.
- References-and-reachability marked non-normative; orphan policy
conformance-neutral.
- NIP-01 retention prose aligned with upstream's MUST/SHOULD wording
("relays SHOULD return only the latest; some may retain older
versions").
Test vectors: events 1-3 are byte-identical to the prior revision
(none of those derivations include the dropped fields). Event 4's
body shrinks, so its NIP-44 content, sha256, id, and Schnorr signature
move. All four ids self-derive and all four sigs verify under
schnorr-verify (script: vectors-30174-noindex.py).
NIP file is 251 lines, well under the 300-line ceiling.
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
tlongwell-block
added a commit
that referenced
this pull request
May 14, 2026
* origin/main: docs(nips): NIP-AE — Agent Engrams (#575) Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
tlongwell-block
added a commit
that referenced
this pull request
May 14, 2026
Pulls in 8 commits from origin/main: - 1858e98 fix(desktop): drive unread badges from live subscription, not refetched lastMessageAt (#581) - 9e76a08 fix(desktop): refine header scaling and shadow (#573) - b74ec95 fix(desktop): keep day dividers below header (#574) - aad564b Move agent activity below composer (#579) - bda98da docs(nips): NIP-AE — Agent Engrams (#575) - 1b87a09 refactor: extract shared @mention resolver into sprout-sdk (#580) - 2ee7356 fix: add default-run to sprout-relay so `cargo run -p sprout-relay` works (#577) - f0549b5 feat(desktop): channel hover state and right-click mark-unread context menu (#578) No conflicts. * origin/main: fix(desktop): drive unread badges from live subscription, not refetched lastMessageAt (#581) fix(desktop): refine header scaling and shadow (#573) fix(desktop): keep day dividers below header (#574) Move agent activity below composer (#579) docs(nips): NIP-AE — Agent Engrams (#575) refactor: extract shared @mention resolver into sprout-sdk (#580) fix: add default-run to sprout-relay so `cargo run -p sprout-relay` works (#577) feat(desktop): channel hover state and right-click mark-unread context menu (#578)
tlongwell-block
added a commit
that referenced
this pull request
May 15, 2026
* origin/main: (33 commits) dev-mcp: add view_image tool (#602) fix(relay,desktop): only advertise NIP-43 when enforced; probe pairing by supported_nips (#601) fix(desktop): derive unread state from NIP-RS + relay catch-up only (#599) docs(testing): rewrite TESTING.md for current API and CLI-first workflow (#597) fix(agent): fix OpenAI-compat request body serialization and max_tokens (#595) feat(desktop): per-persona and per-agent env var overrides (#594) fix(desktop): stop pinning agents to deprecated SPROUT_ACP_TURN_TIMEOUT (#592) fix(desktop): populate member_count in get_channels so channel browser shows real counts (#548) fix(desktop): autofocus message composer on channel/thread open (#572) refactor(cli): restructure flat commands into 12 subcommand groups (#585) feat(sdk): add builder functions for workflows, DMs, and presence (#589) feat(desktop): add message more-actions dropdown menu (#590) fix(mobile): preserve channel list across background/resume reconnection (#588) Redesign Home as an inbox (#582) fix(desktop): drive unread badges from live subscription, not refetched lastMessageAt (#581) fix(desktop): refine header scaling and shadow (#573) fix(desktop): keep day dividers below header (#574) Move agent activity below composer (#579) docs(nips): NIP-AE — Agent Engrams (#575) refactor: extract shared @mention resolver into sprout-sdk (#580) ... Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
This was referenced May 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A draft NIP — NIP-AE, Agent Engrams — for AI agents to keep persistent, structured memory on Nostr. The whole spec is one file:
docs/nips/NIP-AE.md. No code changes.The convention is small on purpose: a dedicated addressable kind plus a few mechanical rules. No relay changes, no new crypto, no negotiation. Two implementations following the spec from text alone converge on bit-identical events.
The model in one paragraph
Memory is scoped to a
(pubkey_a, pubkey_o)pair — one agent, one owner. The agent is the sole signer; the agent and owner share a NIP-44 conversation key, so both can decrypt every record. The owner is a first-class reader by construction, not by permission. Two record types:core(one per pair, holds identity/rules/goals) andmemory(zero or more, one logical entry each). Both ride akind:30174envelope, differing only by the slug at which they are addressed. Slugs are never visible on the wire — thedtag isHMAC-SHA256(K_c, "agent-memory/v1/d-tag" || 0x00 || slug). Tombstones are an in-band write withvalue: null. Cross-record references use wiki-link syntax[[mem/foo]]extracted by literal substring; a reachability graph rooted atcore.profile(non-normative) gives implementations a deterministic answer to "which memories are referenced from the agent's identity surface," and anything outside it is an orphan surfaced for review.Why a dedicated kind (30174)?
Earlier drafts rode
kind:30078(NIP-78 application-specific data) and demultiplexed via the validity gate. That worked — the spec was honest about it — but it shoved a coordination concern (sharing an address space with arbitrary other apps on the same author) into the protocol when we didn't have to.A dedicated kind:
Choice of
30174is mnemonic:30 || 0xAEwhereAEis Agent Engrams — the same hex encoding the NIP indicator uses. Verified unused against the kinds table innostr-protocol/nipsand unmentioned in the broader repo.Design discipline (what the spec actually pins)
The structural moves are what makes it small:
created_atper slug (max(now, T_head + 1)) defeats NIP-01's same-second tiebreak under NIP-44 random nonces. A head whosecreated_atis implausibly far ahead of wall-clock time SHOULD be treated as clock-poisoned and surfaced as a conflict rather than published; the threshold is left to implementations.{slug → {event_id, created_at}}cache to accelerate listing, but it is implementation-local and outside the wire format; an in-band index field was dropped after review because it created a concurrent-writer conflict surface for no protocol-level benefit.dtags can slot in without a wire change.[[<slug>]]substring extraction — clean machine-extractable signal without forcing the agent to maintain a parallel structured-links field. Marked non-normative; conformance doesn't require honoring it. A companion NIP can make it normative.pubkey_a(write or no-marker entries, validws:///wss://URLs), with an out-of-band fallback when the published list is absent or yields zero usable entries. URL canonicalization is for comparison only (lowercase scheme/host, strip default port, strip empty-path trailing slash, dedupe so two parties compute the same set); connections SHOULD be made to the advertised form so relay-side path/host disambiguation is preserved.ptag carries its usual NIP-01 meaning. Generic NIP-65-aware clients may fan a record out to the owner's read relays; this NIP neither requires nor forbids that. Authoritative discovery is always from the agent's configured relays so owners and observers converge on the same head set.OK(optionally with a short propagation delay) and surface a conflict if the published event is not the head. Best-effort by design: writes arriving after the recompute window remain subject to the eventual-consistency semantics. Fixed-window and "MUST NOT silently retry" policies were softened to SHOULDs.Reference test vectors (embedded, self-verifying)
The spec embeds deterministic values derived from pinned
seckey_a/seckey_o/Schnorr aux/NIP-44 nonces. An implementer can re-derive every event id, signature, ciphertext, andd-value from the spec's own inputs and byte-diff against the spec's own outputs.…01, sec2=…02) soK_ccross-checks for free againstnip44.vectors.jsonline 281 (c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d).e907831f80…) as an external anchor for the aux-interpretation gotcha.d, greatercreated_at), and acoreevent whoseprofilecontains wiki-link references to both memory slugs — exercising wiki-link extraction from bothprofileandvalue, plus a dangling reference to a tombstoned slug.Three documented implementation gotchas — places where independent re-derivations are most likely to diverge silently:
shared_x, not its SHA-256 (some bindings' defaultecdh()hashes it).aux = 0x00…00is not "aux omitted" — it must be passed through theBIP0340/auxtagged hash and XOR'd with the secret. Some libsecp256k1 wrappers defaultsign_customto NULL extraparams and silently skip the XOR.ensure_ascii=False— the moment a body contains a non-ASCII character,\uescapes diverge.The vectors were originally co-produced and cross-validated against an independent implementation by Sami; outputs matched byte-for-byte across two crypto stacks. After moving to kind 30174 the event ids and signatures changed (kind is in the id pre-image); the pubkeys, conversation key, d-tags, and ciphertexts 1–3 are unchanged from those earlier verified vectors. Event 4's body changed again when
core.indexwas dropped, so its ciphertext, id, and signature were regenerated; all four sigs re-verify under Schnorr.What's explicitly out of scope
Provenance / trust levels, attention or working sets, structured links, owner-to-agent directives, admission control. Bodies use a strict-required-fields / permissive-unknown-fields rule so companion NIPs can add these later without breaking v1 readers.
Acknowledged caveats (in the spec, not just the PR)
seckey_acan rewrite or tombstone any record, and can re-deriveK_cagainst every known owner pubkey to decrypt all past and future records for those pairs. Replaceable events leave no protocol-level trace of overwrites on compliant relays. Archival relays may show that rewrites occurred but cannot identify which version is authoritative; the NIP defines no version-chaining mechanism.(pubkey_a, kind:30174, p=pubkey_o)reveals that an account uses agent memory and identifies its owner. Pseudonymous, not anonymous.Risk / scope of this PR
Documentation only. One file added to
docs/nips/. No code paths touched.How it got here
This NIP went through a long iteration in the
#sprout-memorychannel. Highlights of what the design walked back from earlier sketches:linksarrays in favor of literal[[<slug>]]substring references — the agent already thinks in prose, so the references live in prose.core.index(was in earlier drafts of this PR). The walk over configured relays was already authoritative; an in-band cache added a concurrent-writer conflict surface for no protocol-level benefit. Clients that want one keep it locally.Reviewed cross-NIP against NIP-01 / 09 / 44 / 65 / 78 in two parallel passes, then again head-to-head against ten upstream comparators (01, 11, 17, 22, 23, 44, 51, 65, 78, 90). Beats nine; ties NIP-44 in original orientation and loses to NIP-44 in spec-identity mode — a structurally honest result (NIP-44 is a byte-level crypto primitive with full algorithmic pinning; a coordination-layer NIP can't match its correctness density).
After PR open, five rounds of codex review against the upstream NIPs produced the structural simplifications committed as
93d2c46: droppingcore.index, decoupling theptag from a NIP-65 carve-out, softening write-verify and clock-poison from MUSTs to SHOULDs, splitting URL canonicalization (compare-only) from URL connect (advertised form), and marking the reachability graph non-normative. Codex never crossed 8/10 on the scored rubric; each pass pivoted to a new "major," which read as the rubric flooring drafts at 8 against "merge upstream as-is" rather than a genuine remaining defect. Shipped at 8.