SuperPi.Memory (super_pi v0.1.0)

Copy Markdown View Source

Durable notes that outlive a conversation: the store behind SuperPi.Plugins.Memory.

Notes live in the memories table, not in a durable document and not in artifacts. A document rewrites the whole corpus as one base row at every checkpoint and pins it in ETS, and artifacts have no search and die with their conversation. A note belongs to a scope {scope_kind, scope_id}: {"owner", principal} by default or {"global", ""}. It is not forked and not rewound, so a note that a branch writes is visible to its parent. Read-only subagents cannot write one, because memory_write carries the state_write effect and plan mode blocks it.

Writing

write/3 validates, then inserts or updates in one Storage.commit/1 (the index triggers run in the same transaction). Limits and rules:

  • Title and body are stored byte for byte. They must be valid UTF-8 without NUL (PostgreSQL cannot store NUL) and the body is capped at :max_body_bytes (32 000 by default).
  • An explicit slug upserts. A slug derived from the title never overwrites: a taken one gets -2, -3.
  • pinned is refused for compaction provenance and for a note with source.url, unless the caller passes allow_pin_untrusted: true. A pinned body goes into the system prompt, so anything from the open internet needs an explicit opt-in. An update takes pinned from the call (default false), so rewriting a pinned note unpins it.
  • Provenance is not model-settable: web when source.url is set, else agent. Code passes :provenance (user, compaction, import).
  • The result lists similar live notes with a score, which keeps the store from becoming a junk drawer. The score is the Jaccard similarity of title and topic words, which does not depend on the search backend.

Reading and linking

search/3 is lexical (SuperPi.Memory.Search). read/2 returns a note with its links. link/5 adds a related, derived_from or supersedes edge. supersedes marks the older note superseded, refuses a cycle and refuses a note that already has a superseder.

Configuration (config :super_pi, :memory, [...]): :max_body_bytes, :index_token_budget, :pinned_body_bytes, :half_life_days, :recency_weight, :pinned_bonus, :duplicate_threshold, :search (backend module), :harvest_model, :harvest_max_facts, :harvest_llm.

Summary

Functions

A configuration value, with the module default.

{count, max updated_seq} of the scope: cheap proof that the index text is still current.

Human-readable message for an error from write/3 or link/5.

The note with slug in the scope, or nil.

Adds a rel edge from the note from_slug to to_slug. supersedes marks to as replaced by from. Idempotent for an existing edge.

Normalizes topics to a space-separated string of lowercase words. Anything outside letters, digits, ., _ and - becomes -. At most 8 topics of 40 characters.

Reads a note with its outgoing links, its backlinks and the slug of its superseder. Returns {:ok, %{memory, links: [{rel, note}], backlinks: [{rel, note}], superseded_by: slug | nil}} or {:error, :not_found}.

The scope of a conversation: its owner's notes by default, or the shared global scope.

The query of every note in a scope.

Derives a slug from a title: ASCII words joined by -, or a short hash when no ASCII word is left.

A short window of body around the first query word, or the start of the body. Whitespace is collapsed.

The topics of a note as a list.

Writes a note. attrs has string keys: "title", "body", and optionally "slug", "topics" (list of strings), "pinned", "confidence" (0..1) and "source" (map with url, artifact, note).

Types

scope()

@type scope() :: {String.t(), String.t()}

Functions

config(key)

@spec config(atom()) :: term()

A configuration value, with the module default.

fingerprint(scope)

@spec fingerprint(scope()) :: String.t()

{count, max updated_seq} of the scope: cheap proof that the index text is still current.

format_error(other)

@spec format_error(term()) :: String.t()

Human-readable message for an error from write/3 or link/5.

get(arg1, slug)

@spec get(scope(), String.t()) :: SuperPi.Durable.Schemas.Memory.t() | nil

The note with slug in the scope, or nil.

link(scope, from_slug, to_slug, rel, opts \\ [])

@spec link(scope(), String.t(), String.t(), String.t(), keyword()) ::
  {:ok,
   %{
     from: SuperPi.Durable.Schemas.Memory.t(),
     to: SuperPi.Durable.Schemas.Memory.t(),
     rel: String.t()
   }}
  | {:error, term()}

Adds a rel edge from the note from_slug to to_slug. supersedes marks to as replaced by from. Idempotent for an existing edge.

normalize_topics(list)

@spec normalize_topics(term()) :: String.t()

Normalizes topics to a space-separated string of lowercase words. Anything outside letters, digits, ., _ and - becomes -. At most 8 topics of 40 characters.

provenances()

read(scope, slug)

Reads a note with its outgoing links, its backlinks and the slug of its superseder. Returns {:ok, %{memory, links: [{rel, note}], backlinks: [{rel, note}], superseded_by: slug | nil}} or {:error, :not_found}.

rels()

scope(conversation_id, opts \\ [])

@spec scope(integer() | nil, keyword()) :: scope()

The scope of a conversation: its owner's notes by default, or the shared global scope.

scoped(arg)

The query of every note in a scope.

search(scope, query, opts \\ [])

Searches the scope. See SuperPi.Memory.Search.run/3.

slugify(title)

@spec slugify(String.t()) :: String.t()

Derives a slug from a title: ASCII words joined by -, or a short hash when no ASCII word is left.

snippet(body, tokens, width \\ 160)

@spec snippet(String.t(), [String.t()], pos_integer()) :: String.t()

A short window of body around the first query word, or the start of the body. Whitespace is collapsed.

topic_list(memory)

The topics of a note as a list.

write(scope, attrs, opts \\ [])

@spec write(scope(), map(), keyword()) :: {:ok, map()} | {:error, term()}

Writes a note. attrs has string keys: "title", "body", and optionally "slug", "topics" (list of strings), "pinned", "confidence" (0..1) and "source" (map with url, artifact, note).

Options: :conversation_id (origin), :provenance, :allow_pin_untrusted.

Returns {:ok, %{memory: note, created: boolean, similar: [%{slug, title, score}]}} or {:error, reason} (see format_error/1).