Release notes
Every release, and the reasoning behind it.
mem-port ships small and often. These are the real notes, including the release that shipped a feature nothing could render, the one that fixed a blank panel caused by our own build script, and the one whose deployment files refused to start. This audience can tell the difference between a changelog and a marketing page, so this is the former.
v1.0.2
Current release on npm. Install or upgrade with the command below.
npm install -g @rsl-innovation/mem-port@latest1.0.2
FixThe deployment files did not work
This is a fix release for three defects shipped in 1.0.0 and 1.0.1. All three were found by running the deployment manifests rather than reading them, which is the only reason they were found at all.
The build was bundling pg. It is an optional dependency loaded through a dynamic import, but it was not marked external, so the build rewrote that import into a generated chunk. That defeats the point twice over: roughly 180 kB of a driver most installs never load shipped to everyone, and the runtime check for whether pg is installed could no longer answer honestly, because the import no longer resolved against node_modules at all. Live in 1.0.1, fixed here.
The error message lied. The catch around that import reported "pg is not installed" for any failure whatsoever, and said exactly that while pg sat in node_modules, installed and importable. It cost real debugging time pointed in the wrong direction. Only a genuine module-not-found error means "not installed" now; anything else propagates with its real cause. An error that misdiagnoses is worse than one that simply propagates.
Both Compose files refused to start. Authentication became required when a deployment binds anything other than loopback — and that landed after the deployment files had been written and verified. So 1.0.0 and 1.0.1 both shipped manifests that fail on startup with "no admin account exists and none can be created". Both now set a bootstrap admin and require the password to be supplied rather than defaulting it, because a shipped default admin password is exactly the kind of thing that survives into production.
- deployments/README.md still claimed a hosted SurrealDB was required, which stopped being true the moment 1.0.1 added the Postgres driver.
- An audit of every MEM_PORT_* variable the code actually reads against what the deployment files document found MEM_PORT_ENV_FILE and MEM_PORT_STORE documented nowhere, and MEM_PORT_PUBLIC_URL missing from the Cloud Run manifest, which is precisely the case that needs it.
- The deployment files have now been wrong twice for the same reason: verified once when written, then silently invalidated by a later change. Nothing builds the image or starts the stacks automatically yet, so treat the manifests as stale after any change to authentication, configuration, or packaging until that gap is closed.
- The 0.7.0 to 1.0.2 upgrade was run against a real library written by 0.7.0 — 90 entities, 73 memories, 30 skills, 23 ADRs, 301 edges. Zero records lost, semantic search still working over the embeddings 0.7.0 wrote, and no client configuration changed, because the daemon is on loopback and auth stayed off.
1.0.1
FeatureA Postgres driver
The storage contract 1.0.0 extracted, now tested by putting a second engine behind it. The default is unchanged: embedded SurrealDB, nothing to install, nothing to run. Postgres is opt-in — npm install pg, then start the daemon with MEM_PORT_DB_URL=postgres://user:pass@host:5432/memport mem-port serve.
Nothing above the driver directory changed to make it work. The only file touched outside src/db/ is the config module, for a driver name and a URL scheme. No tool, service, or port file was modified at all. That is the useful measurement of whether last release's refactor was real: 1,905 lines of Postgres against 1,921 of SurrealDB, implementing the same 76 contract methods, and everything above them indifferent to which one is loaded.
One asymmetry would have leaked straight into tool output. SurrealDB returns undefined for an unset optional field and Postgres returns an explicit null, and JSON.stringify omits the first while emitting the key with a null value for the second. Without a conversion at the boundary, every entity without a summary and every ADR without consequences would grow a key under one driver that the other never produces. One conversion, and a test that proves it.
- Each workspace gets its own Postgres schema, so isolation is structural — the same way SurrealDB gets a database per library — rather than depending on nobody ever forgetting a WHERE clause.
- pgvector is required and checked when the driver connects. Every search mem-port offers is a cosine similarity, so a fallback that sequentially scans would quietly make search performance depend on a server detail nobody remembers checking. mem-port attempts CREATE EXTENSION itself, which works on most managed services.
- pg is an optional dependency, imported lazily, so anyone on the embedded default never downloads a driver they will not load.
- The two drivers are interchangeable, and that is enforced rather than claimed: one test seeds the same fixture through both engines and compares every read tool's output byte for byte, masking only record ids, timestamps, and float noise. It was confirmed to have teeth by breaking the null conversion on purpose and watching it fail, because a passing equivalence test that cannot fail is worth nothing.
- Not covered: a real managed Postgres, as opposed to the pgvector Docker image, and the admin path against Postgres rather than SurrealDB.
1.0.0
Not a breakPluggable storage, accounts, and an admin portal
Start with what did not change, because a 1.0.0 reads like a breaking release and this one is not. Running mem-port locally is identical: still loopback, still no authentication, still embedded SurrealDB with nothing to install, and the MCP tool surface is byte-identical — the same 19 tools returning the same output. Nobody has to do anything. 1.0.0 is a maturity statement, not a break.
What changed is underneath. All 19 tools issued raw SurrealQL, so engine specifics had leaked everywhere: record-id objects across seven files, graph projections, the cosine-similarity call, raw date values handed straight to JSON.stringify. Each tool also declared its own row shape, so the ADR row existed four times in four different versions. Storage now sits behind a contract expressed in domain terms with no query language in it, SurrealDB is one implementation of that contract, and the policy that was never storage in the first place — skill version history, ADR numbering — moved out to its own layer. Adding an engine became a directory and one case in a factory, which is what 1.0.1 then did.
The method is the part worth repeating: build the byte-level golden test first. Thirteen snapshots of every read tool's output, volatile values masked, held identical through the entire refactor. The suite that already existed asserts fields, and a field assertion passes happily when a key that used to be absent starts arriving as null — which is exactly the failure a storage swap produces.
Accounts arrived with the same principle: authentication follows exposure. It is off on loopback, where the operating system is already the boundary, and required on any other interface, where there is none. With it on, an admin portal at /admin manages workspaces, users, API keys and their rotation, and per-workspace grants; it serves its own documentation, and includes a read-only graph explorer for each workspace. Being an admin is deliberately not data access. Admins decide who may reach what, which is a different power from reading it, so a stolen admin password exposes the account model rather than every knowledge graph, and an admin who wants a workspace has to grant it to themselves, visibly.
- Two authentication bugs found and fixed on the way. A library-id header naming the internal credential store resolved straight onto it — reserved names are now refused after normalization, so padded and differently-cased spellings cannot slip past either, and again at the HTTP layer. And because base64url contains underscores, the first API-key parser, which split on the underscore, rejected roughly half of every key it would ever issue, as an intermittent auth failure.
- A pre-existing race in the migration guard: two callers reaching a not-yet-migrated database both ran the schema setup, and concurrent DDL fails with a write conflict. The guard was a flag consulted before an await, so both callers saw it unset. The whole open is memoized per database now.
- Hosted SurrealDB needs server 3.0 or newer and a WebSocket URL — two separate gates, both checked at startup so a misconfiguration fails once with an explanation rather than on every call. Sessions and transactions are 3.0 server-side features and mem-port needs both structurally; the HTTP engine has neither at any version. Reading the client's feature flags produced a confident, wrong answer here. Running against a real 2.x container is what found it: it connects cleanly, then fails on every single request.
- The graph explorer needed real data to design. At five entities a ring layout looked fine. At twenty-six it merged two entity names into one unreadable string, clipped the label of the most-connected entity, and turned thirty-six relations into a hairball. It was rebuilt as a chord diagram, where labels rotate to their own radius so collisions are structurally impossible and the viewBox is measured from the longest label rather than from the ring.
- A container image, a Compose stack, and Cloud Run manifests ship under deployments/, all defaulting to closed. They did not actually start — see 1.0.2.
- Still open: relate_entities rejects any non-empty attributes object, while its own tool description shows one as an example. The golden test found it, and it was deliberately left out of a refactor whose entire premise was zero change to output.
0.7.0
Fixsave_skill revises a skill instead of forking it
save_skill created a new record every time, so saving twice under one name produced a second row rather than revising the first. get_skill resolves a name to a single row, so once duplicates existed it could hand back the older version. mem-port's own instructions tell agents to save skills proactively, which means any agent following them ran into this.
It now updates in place. The record id stays stable, the version it replaced is archived rather than destroyed, and any duplicates already in a library get collapsed on the next write.
- Revising a skill keeps its record id, and the replaced version stays reachable by id.
- get_skill by name resolves to the live version only.
- Entity mentions are replaced rather than merged, so dropping an entity_ref removes it from the graph.
- No unique index on skill.name: forget_skill soft-archives, an archived row keeps its name, and a constraint would then refuse to let you re-create a skill you had forgotten. The rule lives in the write path instead.
0.6.0
Behavior changeSkill bodies on demand, and a rebuilt result panel
list_skills and search_skills no longer return the full procedure body. Both get called at the start of a task to answer one question (is there a skill for this?), and returning every body loaded an entire library into context to answer it. get_skill serves the body for the one you actually picked.
The MCP Apps result panel was rebuilt at the same time. Results paginate five at a time with a running tally, and the panel now reads the host's own style variables and fonts, so its colors and typeface come from the client rather than an approximation of it. Bordered cards became a hairline list, which carries the same information in far less height.
- list_skills on a real 21-skill library: 68.5 kB down to 12.1 kB, 82 percent smaller.
- search_skills on the same library: 33.9 kB down to 5.7 kB, 83 percent smaller.
- search_adrs and list_adrs have the same problem and were deliberately left out of scope.
- list_episodes cannot be trimmed the same way without a get_episode tool, which does not exist yet, so stripping the body there would make it unreachable rather than deferred.
0.5.1
FixThe result panel rendered blank, and it was our bug
The panel showed nothing in Claude Desktop. A proxy trace cleared the host immediately: it advertised the UI extension correctly, fetched ui://mem-port/results.html, then called the tool. The page it fetched was corrupt.
The build inlined the JavaScript bundle into the page with String.replace, passing the bundle as a replacement string. That form treats the dollar sign as a special character, and minified zod is full of regex end anchors, so each one spliced a copy of the page into its own script tag. Thirteen copies of the doctype ended up inside the bundle, which then did not parse. The fix is a replacement function, which turns the substitution off.
The tests could not have caught it. They asserted the page had a script tag and loaded nothing from another origin, and both were still true of the corrupt build. What was missing was any check that the bundle parses at all. That check now exists.
v0.5.1 on GitHub →0.5.0
Replaces A2UIRead tools render through MCP Apps
0.4.0 shipped A2UI surfaces on the nine read tools. Nothing rendered them. Claude implements MCP Apps (io.modelcontextprotocol/ui), and no Claude surface speaks a2ui+json, so a correct implementation of a real published spec drew nothing anywhere while costing tokens on every read call.
MCP Apps inverts the mechanism, and the inversion is the whole design. Rather than attaching a rendered surface to each result, every read tool declares a UI resource in its definition, pointing at one ui://mem-port/results.html page. The host fetches that page, possibly before the tool even runs, renders it in a sandboxed iframe, and pushes the result in. One template serves all nine tools, and the per-call data rides on the result instead of the markup.
- Claude Desktop, Claude web, VS Code Copilot, ChatGPT, Cursor and Goose render the panel.
- Claude Code is not an MCP Apps host, so results stay as text there. The same daemon serves both, so one tool call can look different depending on which client made it.
- The page ships fully self-contained, because hosts render ui:// resources under a deny-by-default policy that blocks every other origin, web fonts included.
- On by default. Off per client with an mcp-apps: 0 header next to library-id, or daemon-wide with MCP_APPS=0.
0.4.0
Removed in 0.5.0A2UI surfaces on the read tools
The nine read tools appended an A2UI v1.0 message stream beside their JSON, so a renderer-capable host could draw results instead of showing a human the model's payload. It lasted one release.
It is listed here rather than quietly dropped, because the reasoning is the part worth keeping: protocol adoption is a separate fact from protocol existence, and only a client support matrix answers it. The implementation conformed to the spec and was verified against a live daemon. That said nothing about whether any host would draw it.
v0.4.0 on GitHub →0.3.0
BreakingNode 22 becomes the support floor
Node 20 reached end of life on 30 April 2026, but mem-port still declared support for it, built against it, and ran CI on it. The engine requirement moves to Node 22 or newer, the build targets 22, and CI runs on both 22 and 24.
Node 20 users can no longer install mem-port, which is why this shipped as a minor bump rather than a patch. The floor is worth revisiting around April 2027, when Node 22 reaches end of life in turn.
v0.3.0 on GitHub →0.2.1
Release processPublishing moves to a tag-triggered workflow
Pushing a version tag now verifies the tag matches package.json, re-runs typecheck and tests, publishes to npm with provenance through OIDC trusted publishing, and creates the GitHub release. No token is stored anywhere, and nothing is published from a laptop.
The first attempt aborted before publishing: the job installed the latest npm, which resolved to a version requiring a newer Node than the runner had. The npm major is pinned explicitly now, which is the point of a release pipeline you can read.
v0.2.1 on GitHub →0.2.0
FeatureThe ADR log
Architectural decision records join memories, episodes, entities and skills as the fifth record type, adding save_adr, search_adrs, list_adrs, get_adr and forget_adr and bringing the tool surface to 19. An ADR holds the context that forced a decision, the decision, its consequences, and the alternatives that lost.
Numbers are sequential within a library, and recording a newer decision with supersedes marks the older one superseded and links the two, so a reversal leaves a readable chain instead of two contradictory records. Search embeds title, context and decision together and deliberately leaves out consequences and alternatives, because people search by the problem rather than the answer.
v0.2.0 on GitHub →
Full release notes and diffs for every version live on GitHub. mem-port follows semantic versioning: a minor bump is where anything that could break an existing install lands.