npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@joe-broadhead/opencode-mem

v0.8.3

Published

Durable private and shared-space memory for OpenCode agents

Readme

opencode-mem

ci

Durable private and shared-space memory for OpenCode agents.

opencode-mem is an OpenCode 2 plugin. It pins a small context block per session, captures completed turns, mirrors skills into OpenCode's native registry, and exposes recall as tools the model calls. It talks to a TencentDB Agent Memory backend over the upstream /v3 contract; the transport is isolated in src/backend/v3.js.

Requirements: OpenCode 2.1.4 or newer and Node.js 22.13 or newer for the repository, operations scripts, and control provisioning. The plugin refuses older OpenCode hosts rather than degrading silently.

Current architecture

  1. The OpenCode plugin talks HTTPS to the public auth/gateway with a Better Auth API key; each machine keeps its own LLM provider.
  2. The gateway verifies the caller, resolves a context from the internal control plane, and routes /core/* and /knowledge/* to one memory cell.
  3. The control plane owns organisations, spaces, members, collections, resources, grants, cells, and placements; the cell owns memory, wiki, code graph, and skills.
  4. The gateway enforces the active space role, the resource catalog, cell status, and a per-cell credential before forwarding. Client identity and key headers are discarded.
  5. Mission Control is served from the same origin; control mode adds space, grant, and member views. See docs/architecture.md.

What the agent keeps

Memory is the person's continuity: it knows how they like things done, does not re-ask what was decided, keeps promises on time, and can say where a project stands (memory-value-design.md).

| What | Where | Tool | |---|---|---| | Preferences, constraints, decisions with their reason, verified facts, short procedures | Atoms, private by default; supersedes replaces an old one | mem_remember | | Things someone will do, with a due date or a trigger: the person's promises and reminders, the agent's, and what others owe | Commitments, private, in the personal space's reserved "Memory records" wiki | mem_commitments | | Where a project stands, its decisions, open questions and people | The project page's dated Status section, in the space's one knowledge wiki | mem_wiki | | What was said | Captured turns, automatically | — |

The pinned block carries a Due list: overdue commitments and those due in the next two working days, at most five lines, unchanged within a day. With a tracker connected, team work items stay in the tracker and memory keeps the link. The usage skill (skills/mem.usage/SKILL.md) has the workflows and page templates.

Quick start

Prerequisites: Node.js 22.13+, OpenCode 2.1.4+, and a cloned repository.

git clone https://github.com/joe-broadhead/opencode-mem.git ~/opencode-mem
cd ~/opencode-mem
npm test                 # every test/*.test.js suite; no network
npm run stub             # http://127.0.0.1:8420, JSON storage in stub/data

In another terminal, install the plugin against the simulator:

node scripts/install.mjs --options '{"baseURL":"http://127.0.0.1:8420","user":"me"}'
opencode service restart
opencode run --auto "Call mem_status and reply with its output verbatim."

The installer backs up ~/.config/opencode/opencode.json and preserves every other plugin. Remove it with node scripts/install.mjs --remove. For a single-machine walkthrough, see docs/local-quickstart.md. For a team deployment, follow docs/team-onboarding.md.

Daily use: load a release, not a working tree

OpenCode loads the plugin from the directory in package, so a working tree there is live: an edit or a branch switch changes every session on the next start. For daily use, export a tagged release and pin both paths to it:

V=0.2.1
DEST=~/.local/share/opencode-mem/releases/$V
mkdir -p "$DEST" && git -C ~/opencode-mem archive "v$V" | tar -x -C "$DEST"

Set the plugin package to $DEST and the instructions entry to $DEST/docs/agent-protocol.md. To upgrade, export the new tag and change both paths; to roll back, point them at the previous directory. Change the paths rather than flipping a symlink: OpenCode reloads a plugin when its config entry changes, and a symlink flip can stop the plugin without loading it again.

From npm

The plugin is published as @joe-broadhead/opencode-mem. The package holds the plugin only: index.js, src/, the built-in skill, and docs/agent-protocol.md. It has no runtime dependencies. Pin an exact version:

{ "package": "@joe-broadhead/[email protected]", "options": { "controlURL": "https://memory.example.com", "token": "<key from /account>" } }

The agent protocol is in the package at docs/agent-protocol.md. Name that file in instructions, or use a release export (above) when you want both paths under your control. The server side (gateway, control, the cell) is not in the package; deploy it from this repository.

Plugin configuration

For a control-plane deployment, point controlURL at the public gateway origin, not at the internal control service. token is the teammate's Better Auth API key.

{
  "plugins": [
    {
      "package": "/opt/opencode-mem",
      "options": {
        "baseURL": "https://memory.example.com/core",
        "knowledgeURL": "https://memory.example.com/knowledge",
        "controlURL": "https://memory.example.com",
        "token": "{env:MEMORY_TOKEN}",
        "agent": "auto",
        "captureVisibility": "private"
      }
    }
  ]
}

| Option | Default | Meaning | |---|---|---| | baseURL | <controlURL>/core; without a gateway http://127.0.0.1:8420 | Memory core or gateway /core URL | | knowledgeURL | <controlURL>/knowledge; without a gateway baseURL | Wiki / code-graph service | | controlURL | MEMORY_CONTROL_URL; inside Jelly the Jelly account's memory origin | Public gateway origin for spaces, context, and turn capture (/v1/capture) | | token | MEMORY_TOKEN; inside Jelly a Jelly token, then the pasted key | Better Auth API key. Order: Running inside Jelly | | capture | true | Capture completed turns. false keeps the tools and captures nothing | | captureVisibility | private | Completed turns stay personal unless explicitly shared. team writes a turn to the session's bound shared space; the gateway decides, and a session with no shared binding keeps the turn private | | captureSubagents | false (MEMORY_CAPTURE_SUBAGENTS) | Capture and pin subagent (child) sessions. Off: a child's brief is never stored as a user turn, and the child gets the protocol but not the pinned block | | compactionCheckpoint | follows capture | Write a private pre-compaction checkpoint to L0. Set it to true to keep checkpoints with capture off | | timeZone | MEMORY_TIME_ZONE; else this machine's zone | The person's IANA time zone: commitment dates are resolved, shown, and put in the Due list in it | | now | the real clock | A fixed clock start (ISO date-time) for tests and evals; time runs forward from it. Never set it in daily use | | stateDir | ~/.local/state/opencode-mem | Spool, claims, materialised skills, log | | debug | false | Add DEBUG lines to the log and mirror every line to stderr. Leave it off in daily use | | debugFile | stateDir/debug.log | Plugin log. INFO, WARN, and ERROR are always written; mode 0600; rotates at 5 MB into debug.log.1. Query, prompt, and memory text are never logged |

There is no team, private-team, user-key, or admin-URL setting. A control-plane deployment gets every container from the resolved space context. Without controlURL, and outside Jelly, the plugin runs in local direct mode against one fixed local scope for the bundled simulator; the space, grant, and member tools report that a control plane is not configured. With a gateway but no key, the plugin loads and every memory tool answers "Memory is unavailable: …" with the reason; nothing is captured. The full option list and defaults are in src/config.js.

Running inside Jelly

Jelly runs each agent in its own OpenCode 2.2.5 engine. The package is @joe-broadhead/opencode-mem, the name Jelly keys plugins and their secrets by, and its package.json carries a jelly declaration. Inside Jelly the plugin is opt-in: a host has it installed, and a person adds it to an agent.

The memory resource. The declaration names "resources": ["memory"]. Jelly gives an agent's engine the identity accessor for a resource only when one of the plugins given to that agent declares it, and its host answers invalid_target for any other name. resources is not in requires, on purpose: a Jelly that does not know the field ignores it and still loads the plugin, which then uses the pasted key.

Credentials, first match wins (src/credentials.js):

  1. the token option or MEMORY_TOKEN (not set inside Jelly; the engine starts with an empty environment);
  2. Jelly's identity accessor, globalThis[Symbol.for("jelly.identity")], when its resources() lists memory: the host is signed in to a Jelly account whose Jelly ID offers memory, and the agent was given this plugin. The plugin asks for a ten-minute token per chat (below), keeps it in memory until 60 s before it expires, and asks again once when the gateway answers 401;
  3. the Memory key setting: the key pasted from Memory's /account page, read with globalThis[Symbol.for("jelly.plugin-secrets")].get("@joe-broadhead/opencode-mem", "token").

A token per chat (0.8.2). On a Jelly host with members, several people can talk to one agent, and a Jelly token names one person. So the plugin asks token("memory", { session }) with the OpenCode session of the work: a captured turn, a tool call, the pinned block, and the replay of a spooled entry each use the token of the session they belong to, which names the person speaking there. A second person can speak in a chat someone else started, so the token is asked for again at each new user message, and the previous turn is still captured with its own speaker's token; a spooled turn is replayed only while the chat's token names the person who spoke it. A turn with no user message (a wake, a routine) keeps the cached token. Tokens are kept per session, for at most 64 sessions (the least recently used goes first), and a session's is dropped when the session is deleted. Work that belongs to no chat (the skill mirror, and Memory's views on a Jelly before 0.3.3) asks token("memory") as before, which Jelly answers with the agent owner's token while the agent is private. A Jelly older than members (0.3.2) refuses the session option; the plugin then asks as before, one token for the agent, which on such a host is its owner's.

When Jelly refuses the token. token("memory") rejects with a code. Jelly's host maps Jelly ID's 400 to invalid_target, 401 to not_enrolled, 403 to access_denied, and everything else, a 503 included, to unavailable, so the plugin cannot tell an outage from Jelly ID turning memory off. The host holds each refusal for 30 s, and so does the plugin, per chat: Jelly is not asked again for that chat until the 30 s pass.

| Code | What the request does | |---|---| | not_enrolled with the detail not_signed_in | The person speaking in this chat has not linked a Jelly account on this host. Memory is off for this chat only: no credential is used, not the pasted key and not another chat's token; nothing is captured or spooled; each tool and command answers "Memory isn't available in this chat because the person speaking hasn't linked their Jelly account on this host." Other chats are not affected | | invalid_target, not_enrolled (no such detail), unavailable | Uses the pasted key, when one is saved. With no key, it fails with "this agent's Jelly sign-in can't be used for memory: … (<code>), and no memory key is saved in the plugin's settings"; a finished turn is spooled for later | | access_denied | Fails: "the Jelly account is disabled or marked for deletion (access_denied); a saved memory key is not used in its place". No fallback |

Why no fallback on access_denied: Jelly says the account behind this agent is disabled or about to be deleted. Writing on with a pasted key would get round the switch that stops that account, so the plugin stops instead. A 401 from the gateway never switches sources: a refused Jelly token is replaced by a new Jelly token or not at all, and a refused pasted key is not retried.

mem_status and /mem-status show the credential in use, for example credential: plugin-secret (Jelly refused a memory token: unavailable; Jelly is asked again within 30 s), and the log has one warning per refusal (its code, never a token).

Accepted risk. The pasted key belongs to whoever pasted it. A chat whose token Jelly refuses with a code that falls back (invalid_target, unavailable, or not_enrolled without not_signed_in) writes as the key's memory user, into that user's memory, whoever is speaking in it, until Jelly answers again. Paste a key from the same account you sign in to Jelly with, and on an agent shared with others prefer no key at all. A speaker Jelly reports as unlinked (not_signed_in) is never written as the key's user. This relies on Jelly passing the refusal's detail to the plugin: a Jelly whose accessor drops it makes that refusal a plain not_enrolled, which falls back.

Gateway origin: the Memory address setting (controlURL), else the url of the identity accessor's memory resource, during a fallback too. baseURL and knowledgeURL default to <origin>/core and <origin>/knowledge.

A token is never logged, spooled, or written to a file. With no credential the plugin still loads and registers its nine tools and its view RPC; each tool answers one sentence saying why memory is unavailable, and nothing is captured, spooled, or sent.

Icon. The declaration's icon is assets/icon.svg, shipped in the package: a flat brain mark on a square viewBox, made only of the elements and attributes Jelly's cleanPluginIcon keeps. Jelly drops an icon that does not clean, without an error, and draws a monogram instead, so npm run check:jelly runs the file through Jelly's own cleaner.

Settings (jelly.contributes.settings):

| Key | Type | Meaning | |---|---|---| | controlURL | string | Memory address. Empty: the Jelly account's memory origin | | capture | boolean | On by default. Off: the tools still work and nothing is captured, the compaction checkpoint included | | token | secret | Memory key, for a host without a Jelly sign-in that offers memory, and the fallback when Jelly refuses a token (not after access_denied). A saved key reaches the plugin at the engine's next start |

Tools and permissions. Each tool is declared as name: "mem.<tool>", permission: "mem_<tool>" (docs/jelly.md has the list):

| Tool | Permission | effect | readOnly | |---|---|---|---| | mem.recall, mem.status, mem.spaces, mem.grants, mem.members | mem_recall, mem_status, mem_spaces, mem_grants, mem_members | allow | true | | mem.wiki, mem.remember, mem.commitments, mem.promote | mem_wiki, mem_remember, mem_commitments, mem_promote | allow | false | | mem.forget | mem_forget | ask | false |

Why this mapping:

  • Permission is the engine's tool id. OpenCode 2.2.5 names a tool <namespace>_<name>, and a tool that sets no options.permission (none of these does) is asked for under that id. Jelly's core plugin asserts tool.options.permission ?? call.tool, and Jelly's schema derives the same id (name.replaceAll(".", "_")). Tavily's declaration follows the same rule: tavily.tavily_search → tavily_tavily_search. So a person's Allow, Ask, or Off on the plugin's card, and the read-only helpers' fence, act on the id the engine really checks.
  • Forget asks (D17). A delete cannot be undone, so mem.forget is declared ask, and Jelly writes that as the tool's default. The engine asks only when a tool asserts its permission, so mem_forget asserts mem_forget before it deletes (a preview asserts nothing). Outside Jelly the plugin's own default rule (src/permissions.js), placed right after an agent's base allow, makes the same assertion ask; a later rule, such as the person's permission config or an "always" answer, still wins. Every other tool is allow, which writes nothing.
  • readOnly: false for wiki, remember, commitments, forget, and promote, because they write. Jelly denies them to the read-only helpers (researcher, reviewer). mem_wiki reads too, but it is one tool, so the helpers lose its reads.

Team writes ask inside Jelly. A team-visible write asserts the plugin's own approval actions, with the space as the resource: memory.remember, memory.forget, memory.promote, memory.promote.revoke, wiki.write. Jelly's default rules (agents/default/opencode.jsonc) set all five to ask, so a person approves each write to a shared space. Private writes, the default, assert none of them; a forget still asks as mem_forget, so a team forget asks twice in Jelly. These five action names are a contract with Jelly: do not rename one, or add one, without telling Jelly, or a team write runs without a prompt there.

asks. The declaration lists the same five actions in "asks", so Jelly can read from the plugin, not only from its own default rules, which run-time actions to ask a person about. Like resources, it is additive and not in requires: a Jelly that does not know the field leaves it out and still loads the plugin. test/jelly.test.js and npm run check:jelly both compare asks with the actions the tools really assert (scripts/approval-actions.mjs), so the two cannot drift apart.

The declaration has no sign-ins: the Jelly account is the host's sign-in, and the pasted key is a secret setting. npm run check:jelly -- <Jelly checkout> parses the declaration with Jelly's own PluginDeclarationSchema, checks that resources is kept and not required, that asks is not required and is kept when that Jelly knows it, runs the icon through Jelly's cleanPluginIcon, and checks the views (Views in Jelly); test/jelly.test.js (with the shared declaration kit, test/support/jelly-kit.ts) and test/views-contract.test.js check the same in CI.

One memory per person. Every Jelly engine runs the OpenCode agent agent, so a person's Jelly agents read and write one personal memory, under agent agent in their own personal container. That memory is separate from the persona and scenarios of their other OpenCode agents (build, plan, …), which are keyed by agent name. agent is not a reserved name.

Views in Jelly

Memory's screens in Jelly are the plugin's own views (jelly.views), shown with the person's Jelly sign-in: there is no second sign-in, and Jelly holds no screen of its own for memory. Each page is a static file in the package (views/*.html), run by Jelly in a sealed frame with no network; it reaches only this plugin's read-only RPC, and only for the agent it is shown for.

| View | Place | Shows | Grants | |---|---|---|---| | home "Memory" | Home tile, small | Whether conversations are remembered, and how many memories are kept | rpc: memory.overview | | commitments "Commitments" | Home tile, small | Open commitments, overdue first, at most eight: what, when ("Overdue since Tue 3 Nov", never colour alone), and who ("You", "Your agent", "Waiting on Kwame"), each with Mark done… | rpc: commitments.list, open-agent | | memory "Your memory" | This plugin's card in Settings → Plugins | Five tabs: Overview (what memory is, the sign-in in use, the address, Remember conversations, health, counts), Commitments (open ones with Mark done, and those closed recently), Spaces, Wiki (browse and read), Recent (the newest 100 memories, with Forget) | data, rpc: all seven methods, open-agent |

The RPC (src/views/rpc.js, schemas in src/views/contracts.js). The plugin registers it with the engine under its package name, the only id Jelly calls for its views. Jelly holds each call to the agent's rules for the method's permission, the one the matching tool reads under, before the plugin sees it:

| Method | Permission | Reads | |---|---|---| | memory.overview | mem_status | The credential source, the address, capture as this engine runs it, health, the personal space and its counts | | spaces.list | mem_spaces | GET /v1/spaces | | memories.recent | mem_recall | The personal space's newest L1 atoms (/v3/atomic/query), 50 a page, two pages | | wiki.list, wiki.pages, wiki.read | mem_wiki | The personal space's wikis, a wiki's pages, one page (at most 48,000 characters); never the reserved "Memory records" wiki | | commitments.list | mem_commitments | The personal space's commitments (the records wiki's commitments.json): open ones overdue first, or those closed recently |

Views ask for the person who opened them (Jelly 0.3.3 and later; 0.8.3). A view is opened by a person on a device, not in a chat, so a view call has no session of its own. Every method's input therefore names an optional string, jellySpeaker (contract v1.5 §6.1). A Jelly of 0.3.3 or later sees it in the declared schema and fills it with a short-lived speaker id, view_…, naming the person whose device made that one call: the owner, a member with a linked Jelly account, or a person with none. The handler takes it out of the input before the read sees it and asks token("memory", { session: <id> }) for that call, so on a shared agent a member's view shows the member's own memory, and a view still works after another plugin's turn. That token is kept for the call only: never in the per-chat cache, never for a later call, the skill mirror or the spool, and the id never reaches the gateway, the log or a stored record. When Jelly answers not_enrolled with the detail not_signed_in, the person who opened the view has not linked a Jelly account on this host: that call uses no credential at all (not the pasted key, not the owner's or another chat's token), sends nothing, and the view says "Memory is unavailable: you have not linked a Jelly account on this host." Other refusals fall back as for a chat. A Jelly before 0.3.3, or stock OpenCode, sends no jellySpeaker, and a view then asks exactly as in 0.8.2. The id is optional and is never in required; the schemas keep additionalProperties: false. Adding it asks for nothing more: no tool, permission, resource or sign-in changes, so Jelly's declaration refresh applies it with the release and puts nothing under Changes to review.

Every call is a read the tools already make, through the gateway with the credential chain above, so no gateway route is added for the views and the gateway scopes each one to the caller. A view never names a user, a space or a container. Reads are under the engine's own primary agents (ctx.agent.list(), which inside Jelly is agent), or the agent option when it is set. A wiki or page id from another person's memory is refused. With no credential the overview says why and what to do, and every other read refuses with the same sentence the tools give.

Writes go through the agent. Mark done on a commitment fills the composer with "Mark '' as done ()."; the person sends it and the agent closes it with mem_commitments. Forget on a recent memory fills the composer with "Forget this memory: “<its first 80 characters>” ()." The id is all mem_forget needs: it finds the memory under the agent the person is talking to, which is the agent the views read under, so no agent name is written. The person sends it, and mem_forget runs under their rules, as any forget does. Remember conversations stays a declared setting on the plugin's card, on by default. Because memory is on for every Jelly user, the Home tile and the top of the Overview say plainly what it saves ("Memory saves your finished conversations to your private memory"), what private means (only you and your agents; nothing goes to a shared space unless you choose, and Jelly asks first), and where to turn it off (Settings → Plugins → Memory). A view cannot open a setting, so the place is named in words. The Overview also notes when a saved change waits for the engine's next start.

No recall view yet. Jelly runs the memory tools through Code Mode, and a view is not given the result of a call made inside it, so there is no view behind mem_recall, and its result is exactly 0.6.2's. What Jelly would need: jelly-views-later.md.

No pattern in the schemas. The pinned engine's RPC refuses to register a method whose schema has a pattern ("Patterns may block validation and are disabled"), so the schemas bound ids by length and the handlers check their form, refusing a bad one in words. check:jelly and the tests refuse a pattern.

npm run check:jelly checks the views against Jelly's own code: the subset rules, each method's schemas through Jelly's reader with a sample answer of each (test/fixtures/view-answers.js), and each page carrying Jelly's bridge helper as it is (views/jelly-view.js, pasted into every page). With a Chrome, npm run shots:views -- <Jelly checkout> <out dir> draws every screen, in Jelly and Jelly Night, under the policy Jelly serves pages with, and fails on any refusal or page error.

Spaces, grants, and memory

Each user has one personal space, in a memory container of its own: upstream scenarios and persona are keyed by container plus agent, so only a container of its own keeps them private whatever agent names people use. Legacy personal spaces that share one container keep a distinct personal agent per user (docs/design/adr-personal-containers.md). A shared space maps to one upstream container and a set of control-plane grants. Membership and placement are control-plane data; the plugin never invents or upgrades a role.

| Tool | Use | |---|---| | mem_recall | Search L0/L1 memory before re-deriving context. It searches the declared task first and widens to the whole space on a miss, labelling each hit. In control mode it searches the session's space (a bound shared space, otherwise personal) or an explicit space; visibility: "private" forces the personal space. | | mem_wiki | Read, search, create, or update wiki and code-graph knowledge. | | mem_remember | Store a rule, decision, preference, or procedure. Private by default. | | mem_forget | Delete atoms and raw turns by id, under the agent and session that wrote them, with the mem_remember rows that hold only that text, and list every other stored copy with ids; or erase a phrase from the L2 scenarios and L3 persona (preview, then confirm). Irreversible. | | mem_promote | Share private memory with a shared space as a copy (one wiki, with provenance), or revoke a share. | | mem_status | Backend health, active scope, layer counts, skills, and spool. | | mem_spaces | List personal and shared spaces, or show the active context. | | mem_grants | List the grants that apply to you in a space. | | mem_members | List a space's member roster. |

Private writes always go to the personal space. A team write needs an explicitly selected or session-bound shared space with editor-or-higher access, and asks for approval. There is no live per-atom grant: mem_promote copies private memory into one new wiki in a shared space, and mem_promote with revoke deletes the copy and reads it back (ADR). Collection and resource grants cover wikis, code graphs, and skills, never memory; a different memory audience needs its own space (ADR).

Agent guidance has two layers. docs/agent-protocol.md is the short always-on protocol; load it through AGENTS.md or the top-level instructions array in opencode.json. The bundled mem.usage skill is the detailed playbook with a decision table, worked workflows, and failure handling. They are related but not identical: the protocol is the rule layer, the skill is the reference.

Mission Control

The auth service serves the console at /console/ and its API at /api/console/*. It shows memory layers, wiki pages and search, a force-directed wiki graph, code indexes, pipeline state, the crew manifest, and spaces, collections, resources, grants, and members. Grant and member mutations go through the control plane. The browser holds only a Better Auth session or API key; service keys stay server-side. A gateway with Jelly ID as its only way in has no sign-in page: the console and /login say that people use Memory from Jelly. See docs/mission-control.md.

Operations

| Task | Where | |---|---| | Load the always-on memory protocol | docs/agent-protocol.md | | Specify or run memory behavior evals | docs/evals/README.md | | Decide what belongs in memory, wiki, or a tracker | docs/memory-primitives.md | | Understand prompt size and cache behavior | docs/tokenomics.md | | Compare public memory benchmarks and our derived subset | docs/evals/public-benchmarks.md | | Deploy, size, back up, restore, or recover a cell | docs/deployment.md | | Understand the public/private boundary | docs/trust-model.md | | PostgreSQL WAL archiving, PITR, and restore drills (greenfield; no SQLite row copy) | docs/postgres-pitr.md | | Control-plane API and migrations | control/README.md | | Compose, host, and observability assets | deploy/README.md | | Diagnose a failure | docs/troubleshooting.md | | Documentation index | docs/README.md |

Control-plane backup and restore cover control metadata only. Memory, wiki, code graphs, skills, auth sessions, and cell volumes are separate backup sets:

npm run ops:backup -- --db ./control/control.db --out ./backups/control --retain 14
npm run ops:restore -- --backup ./backups/control/<backup-dir> --target ./control/control.db --dry-run
npm run ops:drill

Tests

npm test             # every test/*.test.js: plugin, gateway, console, scripts, ops, load, eval grader
npm run test:control # control API, policy, storage, provisioning, and migrations
npm run test:ops     # subset of npm test: control backup, restore, retention, and drill
npm run build:ui     # build the Mission Control SPA
npm run check:syntax # parse every JavaScript module
npm run check:docs   # relative links and anchors in tracked Markdown
npm run check:docker
npm run check:compose
npm run eval:controls # offline: 62 grader controls
npm run eval:core     # live: 38 core scenarios (needs model credentials)
npm run eval:extended # live: 103 private memory scenarios
npm run eval:public   # live: 22 LongMemEval-derived cases
npm run eval:closed-book # live: 10 empty-memory leakage controls
npm run e2e          # live: six eval scenarios against a temp stub and fake control plane
npm run verify:edge  # deployed edge: every plugin read path reaches a service

Every npm run alias is listed in CONTRIBUTING.md.

The bundled simulator proves contract handling and lifecycle wiring; it does not prove the real backend's extraction or curation quality. Extraction and curation are asynchronous and batched: a same-space recall can return the raw L0 write immediately, but the distilled L1/L2 view can lag.

Contributing and security

See CONTRIBUTING.md for development and checks, and SECURITY.md for the trust model and private vulnerability reporting. Do not put credentials, customer data, or private transcripts in team-visible memory.

License

MIT