|
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 14s
Check merge_group trigger on required workflows / Required workflows have merge_group trigger (pull_request) Successful in 11s
CI / Detect changes (pull_request) Successful in 17s
E2E API Smoke Test / detect-changes (pull_request) Successful in 18s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 25s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 21s
Harness Replays / detect-changes (pull_request) Successful in 19s
pr-guards / disable-auto-merge-on-push (pull_request) Failing after 6s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 14s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 17s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 17s
CodeQL / Analyze (${{ matrix.language }}) (go) (pull_request) Failing after 2m2s
CodeQL / Analyze (${{ matrix.language }}) (javascript-typescript) (pull_request) Failing after 2m2s
CodeQL / Analyze (${{ matrix.language }}) (python) (pull_request) Failing after 2m2s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m4s
CI / Platform (Go) (pull_request) Successful in 11s
CI / Python Lint & Test (pull_request) Successful in 12s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 31s
Harness Replays / Harness Replays (pull_request) Failing after 1m11s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 3m40s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 5m40s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 6m55s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 6m47s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Canvas (Next.js) (pull_request) Failing after 10m3s
The GitHub org Molecule-AI was suspended on 2026-05-06; canonical SCM is now Gitea at https://git.moleculesai.app/molecule-ai/. Stale github.com/Molecule-AI/... URLs return 404 and break tooling that clones / pip-installs / curls them. This bundles all non-Go-module URL fixes for this repo into a single PR. Go module path references (in *.go, go.mod, go.sum) are out of scope here -- tracked separately under Task #140. Token-auth clone URLs also flip ${GITHUB_TOKEN} -> ${GITEA_TOKEN} since the GitHub token does not auth against Gitea. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| pinecone-example | ||
| CHANGELOG.md | ||
| README.md | ||
| testing-your-plugin.md | ||
Writing a Memory Plugin
This document is for operators and ecosystem authors who want to replace the built-in postgres-backed memory plugin (the default implementation that ships with workspace-server) with their own.
The contract was introduced by RFC #2728. The shipped binary is
cmd/memory-plugin-postgres/; reading its source is the fastest way
to see a complete reference implementation.
What the contract is
The plugin is an HTTP server that workspace-server talks to via the
OpenAPI v1 spec at docs/api-protocol/memory-plugin-v1.yaml.
Six endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/v1/health |
GET | Liveness probe + capability list |
/v1/namespaces/{name} |
PUT | Idempotent upsert |
/v1/namespaces/{name} |
PATCH | Update TTL or metadata |
/v1/namespaces/{name} |
DELETE | Remove namespace and its memories |
/v1/namespaces/{name}/memories |
POST | Write a memory |
/v1/search |
POST | Multi-namespace search |
/v1/memories/{id} |
DELETE | Forget a memory |
The wire types are defined in
workspace-server/internal/memory/contract/contract.go. Run-time
validation is built into the Go bindings via Validate() methods —
your plugin SHOULD perform equivalent validation.
What workspace-server takes care of
You do not implement these in the plugin; workspace-server is the security perimeter:
- Secret redaction (SAFE-T1201). All
contentyou receive is already scrubbed. Don't run additional redaction; it's pointless. - Namespace ACL. workspace-server intersects the caller's readable namespaces against the requested list before sending you the search request. The list you receive is authoritative.
- GLOBAL audit. Org-namespace writes are recorded in
activity_logsserver-side; you don't see them. - Prompt-injection wrap. Org memories returned to agents get a
[MEMORY id=... scope=ORG ns=...]:prefix added at the workspace-server layer. Yourcontentfield is plain text.
What you implement
- Storage of
memory_namespacesandmemory_records(or whatever shape you want — Pinecone vectors, an in-memory map, etc.) - The 7 endpoints above with the request/response shapes the spec defines
/v1/healthreporting your supported capabilities (see below)- Idempotency on namespace upsert (PUT semantics, not POST)
- Idempotency on memory commit when
MemoryWrite.idis supplied (see "Memory idempotency" below)
Memory idempotency
MemoryWrite.id is optional. Two contracts to honor:
| Caller passes | Plugin MUST |
|---|---|
id omitted |
Generate a fresh UUID, return it in the response |
id set |
Upsert keyed on this id — if a row with that id already exists, UPDATE it in place rather than inserting a duplicate |
The backfill CLI (memory-backfill) relies on the upsert behavior
so retries don't duplicate rows. Production agent commits leave id
empty and rely on the plugin's UUID generator — the hot path is
unchanged.
The built-in postgres plugin implements this with INSERT ... ON CONFLICT (id) DO UPDATE. A vector-DB plugin (e.g., Pinecone) would
use the database's native upsert primitive on the same id.
Capability negotiation
Your /v1/health response declares what features you support:
{
"status": "ok",
"version": "1.0.0",
"capabilities": ["embedding", "fts", "ttl", "pin", "propagation"]
}
| Capability | What it gates |
|---|---|
embedding |
Agents may ask for semantic search; you receive embedding: [...] in search bodies |
fts |
Agents may pass a query string; you decide how to match (FTS, ILIKE, regex) |
ttl |
Agents may set expires_at; you must not return expired rows |
pin |
Agents may set pin: true; you should rank pinned rows first |
propagation |
Agents may set propagation: {...}; you must store it as opaque JSON and return it on read |
A capability you DON'T list is fine — workspace-server adapts the MCP
tool surface to match. E.g., a Pinecone-only plugin that lists only
embedding will silently ignore agents' query strings.
Deployment models
Three common shapes:
-
Same machine, different process: workspace-server boots, then
MEMORY_PLUGIN_URL=http://localhost:9100points at your plugin running on a unix socket or localhost port. This is what the built-in postgres plugin does. -
Separate container: deploy your plugin as its own service on the private network. Set
MEMORY_PLUGIN_URLto its DNS name. -
Self-managed: customer-owned plugin running on customer-owned infrastructure, accessed over a tunnel. Same env-var wiring.
Auth is none — the plugin must be reachable only on a private network. workspace-server is the only sanctioned client.
Replacing the built-in plugin
This is the canonical operator runbook for swapping the default plugin out. The same sequence applies whether you're swapping for another postgres plugin variant, Pinecone, Letta, or a custom implementation.
-
Stand up the new plugin. Deploy the binary/container, confirm it boots, confirm
/v1/healthreturnsokwith the capability list you expect. -
Run the backfill in dry-run mode to scope the migration:
DATABASE_URL=postgres://... \ MEMORY_PLUGIN_URL=http://your-plugin:9100 \ memory-backfill -dry-runReports row count + namespace mapping per workspace, no writes.
-
Apply the backfill:
memory-backfill -applyIdempotent on retry — the backfill passes each
agent_memories.idtoMemoryWrite.id, so partial-then-full re-runs upsert in place. -
Verify parity before flipping the cutover flag:
memory-backfill -verify -verify-sample=200Random-samples N workspaces, diffs
agent_memoriesdirect query against plugin search via the workspace's readable namespaces. Reports mismatches and exits non-zero if any are found — wire into your CI to gate the cutover. -
Flip the cutover flag. Set
MEMORY_V2_CUTOVER=trueon workspace-server and restart. Admin export/import now route through the plugin; legacyagent_memoriesbecomes read-only. -
Existing data in the old plugin's tables is NOT auto-dropped. Deliberate safety property — operator drops manually after the ~60-day grace window. If you switch back later, old data comes back into use (no loss).
If -verify reports mismatches, do NOT set MEMORY_V2_CUTOVER —
inspect the output, re-run -apply to backfill missing rows (it
upserts, so this is safe), and re-verify.
Worked examples
pinecone-example/— full Pinecone-backed plugintesting-your-plugin.md— running the contract test harness against your implementation
When to write one vs. fork the default
Fork the default postgres plugin if:
- You want different SQL (Materialized views? Different vector index?)
- You want extra auth on top
- You want server-side metrics emission
Write a fresh plugin if:
- The storage backend is fundamentally different (vector DB, KV store, in-memory, file-based)
- You're integrating an existing memory service (Letta, Mem0, etc.)
See also
CHANGELOG.md— contract revisions and fixup waves- RFC #2728 — design rationale
cmd/memory-plugin-postgres/— reference implementationdocs/api-protocol/memory-plugin-v1.yaml— full OpenAPI spec