changelog ·what shipped, not roadmap

the record of what changed.

every 3ngram release with its date and grouped changes, each linked to the source on GitHub.

Updated

v1.4.3

view release ↗

Patch release: a transitive @hono/node-server advisory fix that reaches the MCP HTTP transport. No source changed in any published package and no runtime behaviour moves — the fix is a resolution, carried by the lockfile and by the image built from it.

Changes

  • @hono/node-server moves 1.19.14 → 1.19.17 (GHSA-frvp-7c67-39w9). The advisory is a serve-static path traversal reachable on Windows through an encoded backslash. @hono/node-server is not a declared dependency of @3ngram/server; it arrives transitively as a regular dependency of @modelcontextprotocol/node, which the MCP HTTP transport relies on. Because it is a regular dependency rather than an optional peer, a scoped root override (@hono/node-server@<1.19.15: ^1.19.15) reaches it — unlike hono itself, which had to become a declared dependency in v1.2.6.

    The resolution is 1.19.17 rather than the advisory’s 1.19.15 because 1.19.16 was never published.

    The workspace lockfile is what pins this, and the published ghcr.io/b3dmar/3ngram image builds from that lockfile, so the image carries the patched version from this release forward.

  • Dev-scope advisory overrides re-raised. nanoid 3.3.16 → 3.3.17 (GHSA-2v37-7h3g-55p8) and js-yaml 3.15.0 → 3.15.1 / 4.3.0 → 4.3.1 (GHSA-5p4m-2wfm-xmqj). Both are reached only through devDependencies — nanoid via vitest → vite → postcss, js-yaml via @changesets/cli — so neither is present in any published package or in the server image. The js-yaml selectors had gone inert (3.15.0 no longer matches <3.15.0) and are re-raised to the new advisory boundaries, still split so the unaffected 5.x major stays put. The orphaned fast-uri override, which had left the dependency graph entirely, was pruned; it changed no resolution.

  • A CI gate now re-asserts every dependency override against the lockfile. An override is written once and never re-examined — --frozen-lockfile replays the recorded resolution, so CI passed whether or not an entry still did anything. scripts/check-override-freshness.mjs fails on two outcomes: NOT APPLIED (a resolved version still satisfies the override’s own selector, the optional-peer failure mode that forced hono to become a declared dependency) and ORPHANED (the package is gone from the lockfile). It runs in the hygiene lane with no install, reading only pnpm-workspace.yaml and pnpm-lock.yaml. Repository tooling only — no runtime change, and no package version moves with it.

Upgrading

No configuration or migration changes, and no API changes.

Nothing in the published @3ngram/server, @3ngram/sdk, or 3ngram package contents changed; the version exists to carry the patched resolution. Running ghcr.io/b3dmar/3ngram:1.4.3 is sufficient — the image is built from the lockfile that pins 1.19.17.

Consumers who resolve their own dependency graph rather than running the published image should confirm their own @hono/node-server resolves to >= 1.19.15 — an override or a lockfile refresh, since the vulnerable version is reached transitively there too.

Library versions

@3ngram/core 0.9.2, @3ngram/db 0.8.2, @3ngram/schema 0.7.2, @3ngram/config 0.2.6, @3ngram/llm 0.2.4 — unchanged; nothing in this release touched them.

v1.4.2

view release ↗

Patch release: the advertised MCP tool output schemas open up, so a client holding a cached catalog no longer hard-fails on a response field added after it fetched. Runtime validation is untouched — the server still parses its own results strict — and inputs stay closed. Also in this release, the self-imposed MCP surface caps are replaced by a gated eval that measures what they were standing in for.

Changes

  • Tool output schemas advertise additionalProperties: true (issue #154). Every tool output object was .strict(), which Zod 4 emits as additionalProperties: false in the JSON Schema that tools/list publishes. Clients cache that catalog for an hour, so any release adding a response field hard-failed every validating client for the whole TTL window — and nothing prompted an early re-fetch, because the failure is client-side output validation rather than a -32601/-32602 the client reads as staleness.

    That is the v1.4.1 incident: a session still holding the v1.3.0 catalog called get_facts and died on data/facts/0 must NOT have additional properties — a nested item object, a purely additive field. Every object node reachable in all eleven tool output trees now carries .meta({ additionalProperties: true }): root envelopes, array item objects, union members, and section wrappers alike, since the incident failed at depth rather than at the root.

    The runtime contract does not move. .meta() carries metadata only — the objects remain .strict(), so the server still rejects an unknown key in a result it produced itself, and the search envelope’s projection-homogeneity refinement still relies on its hit members being strict. .loose() and .passthrough() would have moved the runtime contract and were deliberately not used.

    Inputs stay closed, and the asymmetry is pinned by a test. An unknown argument key remains a loud rejection — a silently dropped scope filter reads as a scope leak — so no input schema’s advertisement moved a byte. The briefing/handoff selector union is the one object reachable from both an input and an output tree (it is an argument and an echo); the openness rides an output-side derivation, leaving the input union untouched. A registry invariant test walks every emitted output schema asserting no additionalProperties: false at any depth, and asserts every input root still carries it.

    The OpenAPI response schemas open up too, deliberately. The generator reuses the same output schemas for REST responses, so POST /api/v1/memories, POST /api/v1/search, GET /api/v1/facts, GET /api/v1/briefing, and the revise/resolve responses now publish additionalProperties: true. Same rationale: a REST reader compiled against an older spec should not break on a field that was only added. Request bodies and query schemas are untouched.

  • MAX_TOOLS = 12 and MAX_PROMPTS = 2 are gone; a gated eval replaces them (#155). Both were 3ngram’s own discipline, entered at the v1.0.0 launch commit and sourced to nothing upstream — the specification defines no maximum tool count and paginates tools/list, and the pinned SDK enforces no limit. Neither constant was ever exported past its module and neither appears in the public API report, so there is no runtime or API change for any client: the server still registers exactly 11 tools and 2 prompts, with the same names, schemas, and annotations.

    What replaced them measures the property they proxied for. The tool-selection eval slice moved from report-only to gated: selection_accuracy_at_1 (0.8545) and selection_margin (0.1097) are recorded as floors, and max_description_overlap (0.6737, briefing ~ handoff) as a ceiling — a metric where lower is better, so eval/fixtures/floors.json gained a ceilings block with its own comparison rather than storing an inverted floor. A twelfth tool is no longer blocked; a twelfth tool whose description reads like an existing one now fails a required check, and the failure names the offending pair.

    The docs generator no longer arbitrates the surface — generate-mcp-reference.ts threw above both ceilings, which put the argument in the one place that cannot measure either metric; the generated docs/reference/tools.mdx and prompts.mdx now cite the eval instead. AGENTS.md hard rule 8 became an evidence test (JTBD, regenerated surface snapshot, per-tool eval scenarios) with its enforcement named.

  • @3ngram/core moves for a comment-only change: write/archive.ts justified its REST-only surface by citing hard rule 8 without its reason, which no longer reads correctly now that the rule is an evidence test rather than a count. No behaviour changed in core.

Upgrading

No configuration or migration changes, and no breaking API changes.

Clients that validate MCP tool results against the advertised schema will accept additive response fields from now on, including across a stale catalog window. A client pinned to a locally vendored copy of an older schema should refresh it to pick up the openness; nothing else is required. Because the change is to what the server advertises and not to what it accepts, servers and clients can be upgraded in either order.

REST consumers that generate types from the OpenAPI document will see additionalProperties: true on response schemas. Strict code generators may emit an index signature on those types where they previously emitted a closed object.

Library versions

@3ngram/core 0.9.2, @3ngram/db 0.8.2, @3ngram/schema 0.7.2, @3ngram/config 0.2.6, @3ngram/llm 0.2.4.

v1.4.1

view release ↗

Minor release: structured retrieval. Numbers no longer live only inside prose — facts get a live write path, search learns an exhaustive chronological mode, and bi-temporal range reads turn a fact series into one call. No new MCP tool was added for any of it; every capability extends an existing tool’s query shape, and the tool count stays at eleven.

Structured facts capture

  • remember accepts facts[] — structured facts (subject, predicate, value, optional validity window) written atomically with the memory, in the same transaction, composite-FK’d to it. The response echoes factIds exactly when facts were written; existing responses are byte-identical. Both transports (MCP and POST /api/v1/memories) carry the same contract.

  • The convention is one measure per fact, unit in the predicate: subject lift.back_squat, predicate top_set.weight_kg, value 98. Values stay text; charting consumers cast on read.

  • Fact timestamps are ISO strings at the boundary. A Date-typed field cannot ride the JSON Schema that MCP publishes via tools/list, so the contract is ISO-only with conversion at the handler seam. Validity windows are compared as instants, not strings, so mixed-precision pairs (…00Z vs …00.001Z) behave correctly.

Fact proposals: staged extraction awaiting review

  • A new fact_proposals table stages machine-extracted facts for human review before they become queryable truth. One open proposal per candidate fact (partial unique index over md5(value) — btree-safe for long values), first-proposal-wins collapse semantics, append-and-supersede lifecycle, full RLS with the tenant-qualified composite FK making cross-tenant references unrepresentable.

  • review_proposals reviews both kinds. The list grows an optional factProposals array (edge-only responses are byte-identical); accept and reject take the same proposalId-only input and probe both proposal kinds. Accepting a fact proposal materializes the fact in the same transaction; a concurrent double-apply loses the race cleanly rather than double-writing.

  • The extraction job itself is deliberately not in this release; the seam it will use (an injectable extractor behind the same repository pattern the consolidator uses) ships ready.

Search: supersession visibility

  • Superseded predecessors are demoted for every revise kind. The ranking penalty previously keyed on supersedes edges only; updates revises closed a memory’s validity without demoting it. Both now demote.

  • Every search hit carries superseded: boolean, on both transports and both projections, computed from the same predicate that drives the demotion — a demoted hit can never read false. Superseded rows remain retrievable (demote, never filter); the flag is what tells you which ones they are.

Search: chronological list mode

  • order: "chronological" turns search into an exhaustive, filter-driven enumeration: no query needed (filters required instead), no embedding call, no semantic ranking — newest-recorded first, live memories only, with as_of time travel lifting the live gate when asked. “Every gym session, in order” is now a contract the server actually offers rather than a top-k approximation.

  • The list cursor is small and drift-free. Ranked search’s continuation token carries a frozen candidate pool (~4–6 KB); the chronological cursor is a (recorded_at, id) keyset position. It carries the timestamp at full microsecond precision as opaque text — a millisecond-floored cursor silently skips rows when a batch insert shares one transaction timestamp, and a regression test now pins the exact failure.

  • Cursors are bound to the mode and filters that minted them; replaying a cursor across modes is rejected as a typed mismatch, never silently misread.

Facts: bi-temporal range reads

  • get_facts accepts from/to — a half-open [from, to) overlap window over fact validity, ordered chronologically, returning every generation in the window (superseded ones included: that is what a history is). recordedAt joins the output so consumers can tell when a fact was observed versus when it was written down — retroactive logging stops lying about dates.

  • Range and as_of are mutually exclusive; empty and inverted ranges are rejected; bounds reject sub-millisecond precision and advertise the limit.

Also in this release

  • GET /api/v1/version (authenticated) reports the running server version, so deploy tooling can tell a finished rollout from one still in flight.
  • MCP reference documentation for resources and the tool surface expanded, and the design docs now record the deliberate decision to keep the twelfth tool slot unspent.

Review hardening

Pre-release review (v1.4.0 was versioned but never tagged; this release supersedes it) closed five findings before anything shipped:

  • Account erasure and portability export now cover fact_proposals — staged proposal content is redacted on erasure and included in the export archive like every other user-owned table.
  • Chronological search rejects a query rather than silently ignoring it — the query never applied to the enumeration, so a caller could receive the entire live set believing it was filtered.
  • Fact write timestamps reject sub-millisecond precision (the read bounds already did), so nothing is silently truncated on the way in.
  • The superseded flag and its ranking demotion now require closed validity in addition to an incoming revision edge, so an imported updates edge whose target is still live is neither flagged nor demoted — search now agrees with memory_history about what “superseded” means.

Upgrading

  • One additive migration (0031_fact_proposals) — a new table; no existing table is altered. The migration adds a foreign key referencing memories, so apply it with a lock_timeout and retry rather than queueing behind long-running transactions. Self-hosters on FIPS-mode Postgres builds: the new unique index uses md5(), which FIPS disables.
  • No breaking API changes. All new request fields are optional; all new response fields are additive. Shipped schemas are byte-identical for existing calls.

v1.3.0

view release ↗

Minor release: the MCP surface catches up with the 2026-07-28 protocol. Two specification MUSTs are closed, two protocol primitives the server had never served are now served — resources and completions — and clients finally receive enough description to use the tool surface well rather than guess at it.

There is one new REST endpoint, GET /api/v1/version, so deploy tooling can tell a finished rollout from one still in flight.

Protocol compliance

  • server/discover now advertises a cache hint — the same one-hour cacheScope: public the tool and prompt catalogs carry. The spec requires hints on every cacheable result, and discovery is one of them. This was not a case of the field being merely absent: the SDK’s 2026 codec fills unconfigured hints from its own defaults (ttlMs: 0, cacheScope: private), so discovery was actively advertising itself as immediately stale and clients re-probed on every reconnect.

  • /mcp now validates the Origin header. A request with no Origin is allowed — the spec’s requirement is conditional on the header being present, and no non-browser client sends one — while a present Origin must appear in an allowlist or the request is refused 403 ahead of authentication and rate limiting. Host is deliberately not validated; see Upgrading below, because this one has a configuration consequence.

New protocol surface

  • Memory bodies are readable as resources: threengram://memory/{id}, via resources/templates/list and resources/read. resources/read is cacheable on 2026-07-28, so a client that pulled a truncated: true search hit can cache the full body instead of re-calling get_memories every session.

    The body carries only fields that never change after a write — content, topic, type, scope, project, recordedAt — and deliberately omits lifecycle state (status, validity, commitment status, tags). That omission is what makes a 24-hour cacheScope: private TTL sound: nothing in a cached body can go stale, because supersession mints a new memory with a new id rather than editing the old one. Lifecycle questions stay on the tool calls, which are never cacheable.

    Reads enforce the same tenant, read-scope, and access guards as the read tools, and an id belonging to another tenant is reported identically to one that does not exist. resources/list returns nothing by design — enumerating a tenant’s corpus is the firehose the no-firehose rule exists to prevent.

  • Argument completion (completion/complete) over the tenant’s own scope names, so a client can offer real values instead of making the user recall them. It is an adapter over the existing facets read and carries the same guards: tenant from verified auth rather than the request, memory:read enforced fail-closed, access gate ahead of the read. A caller that may not read completes to an empty list rather than an error.

Describing the server to clients

  • server/discover carries instructions — a short usage policy telling the model to open with briefing, to search before asserting something is unknown, that memory is append-only, and that scope and project decide what later reads return. Until now the only guidance an agent received was eleven individual tool descriptions with no cross-tool framing.

  • Every tool declares annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint), so a client can auto-approve a read like search instead of giving it the same friction as a write. destructiveHint is false on every memory write, which is accurate rather than optimistic: append-and-supersede never destroys memory data.

REST

  • GET /api/v1/version returns the running server’s package version. Nothing previously exposed build identity over HTTP — /health reports liveness and /ready readiness, but neither says which build answered, so a post-deploy probe could verify the previous build and report success.

    Authenticated like every /api/v1 route, so the exact version is never disclosed on an unauthenticated surface, but deliberately ungated: the response is the server’s own build identity, not tenant data, and a deploy probe must stay answerable when the access gate itself is broken.

Upgrading

One configuration change is worth acting on before upgrading a browser client. Origin validation is fail-closed: with neither WEB_APP_URL nor the new optional MCP_ALLOWED_ORIGINS configured, the allowlist is empty and any request carrying an Origin header is refused 403.

This is safe for normal use — Claude Desktop, the CLI, and agent runtimes send no Origin at all and are unaffected — but a browser-based client needs its origin listed. WEB_APP_URL is allowlisted automatically; set MCP_ALLOWED_ORIGINS (comma-separated) for any additional origins. For local development against MCP Inspector, that is MCP_ALLOWED_ORIGINS=http://localhost:6274.

No migrations. No other configuration changes.

Library versions

@3ngram/core 0.8.6, @3ngram/db 0.7.5, @3ngram/schema 0.6.4, @3ngram/config 0.2.6, @3ngram/llm 0.2.4.

v1.2.7

view release ↗

Patch release: an OAuth correctness fix that stops handing clients a credential the authorization server would never honour, and a correction to the remember tool description that had been telling agents the opposite of what the read surface actually does.

Changes

  • Refresh tokens are no longer issued to clients that never advertised the refresh_token grant (issue #86). The authorization-code exchange minted a refresh token unconditionally, but the token route gates the refresh grant on the client’s advertised grant_types. A client registered for authorization_code alone — which is also the schema default when a CIMD document omits grant_types entirely — therefore received a refresh token that the very next request rejected as invalid_client. Nothing in the token response signalled that, so the client had no way to know the credential was inert until it tried to use it and lost its session.

    Issuance now matches what the authorization server will actually honour: the refresh token is omitted from the response and its hash is not persisted, since a hash for a token no client was ever handed can never be presented or rotated. refresh_token on the token response is now optional, which is what RFC 6749 §5.1 always specified.

    Rotation additionally fails closed if a client’s advertised grants are narrowed between issuance and rotation, rather than revoking the predecessor and minting no successor.

    This predates the CIMD grant-type narrowing in #85 — grant_types: ['authorization_code'] has always been accepted.

  • The remember tool description no longer contradicts the read surface (issue #71). It asserted flatly that “a memory written with a NULL project never matches a project filter”, which stopped being true once the scope_project selector gained includeUnscoped: true. briefing and handoff were updated at the time; remember was missed, so the write surface was describing the opposite of what the read surface does.

    The description now distinguishes the two cases precisely: the bare project selector is still never widened, and only scope_project with includeUnscoped: true opts NULL-project memories back in. It also drops a dangling (issue #244) citation, which referenced an internal tracker item that means nothing to a reader of the public tool surface.

    Tool descriptions are standing context on every MCP connection, so the regenerated surfaces (docs/reference/tools.mdx, eval/fixtures/transport-surfaces.json) and the recorded transport-cost floors move with it: +13 surface tokens, +104 per-task uncached, +26 per-task cache-effective.

  • Threads design doc published at docs/concepts/threads.mdx. Design documentation only — no runtime change and no package version moves with it.

Upgrading

No configuration or migration changes.

One behavioural note for OAuth integrators: a client whose registration advertises only authorization_code will now receive a token response with no refresh_token field. That credential was already non-functional — presenting it returned invalid_client — so nothing that previously worked stops working. Clients that parse the token response should treat refresh_token as optional, per RFC 6749 §5.1. To keep receiving refresh tokens, advertise the refresh_token grant in the client’s grant_types.

Library versions

@3ngram/core 0.8.5, @3ngram/db 0.7.4, @3ngram/schema 0.6.3, @3ngram/config 0.2.5, @3ngram/llm 0.2.4.

v1.2.6

view release ↗

Patch release: dependency advisory fixes, plus a move to Node 24 for how this project is built, tested and shipped. The supported Node floor for consumers is unchanged at >=22.

1.2.5 was versioned but never published — see Skipped version below.

Changes

  • hono is now a declared dependency of @3ngram/server, pinned to 4.12.34. It previously arrived as an optional peer of @modelcontextprotocol/node, which pnpm auto-installs. Two consequences, both fixed here:

    1. It carried an open advisory, and pnpm overrides cannot reach an auto-installed peer — a scoped override, a bare override and pnpm install --force all left the vulnerable version pinned. Only an explicit declaration moves it.
    2. @modelcontextprotocol/node@2.0.0 declares hono: ^4.12.34, so the auto-installed version was violating that peer range, which pnpm peers check reported as an unmet peer.

    Declaring it also means consumers resolve the version this project actually tests against, rather than whatever their own tree happens to auto-install.

  • postcss advisory override raised to 8.5.23. The previous selector (<=8.5.17) no longer covered the advisory range.

  • Built, tested and shipped on Node 24 (Active LTS). CI, the release workflow and the server image base move to Node 24 — node:24-bookworm-slim, digest-pinned so the base is reproducible rather than tracking a moving tag. Node 22 entered maintenance on 2025-10-21 and receives security fixes only.

  • engines.node deliberately stays >=22. What this project builds on and what it requires of consumers are separate decisions. Nothing in this closure uses a Node 24 feature, and Node 22 is supported upstream until 2027-04-30, so raising the published floor would have hard-failed Node 22 consumers on a patch release. Raising it will be a deliberate major when there is an actual reason.

Skipped version

1.2.5 was versioned and tagged but never published to npm or GHCR. Its engines.node raise to >=24 would have broken Node 22 consumers in a patch release; the release gate rejected the tag for missing curated notes before any publish job ran, and the repository’s tag-protection ruleset makes release tags immutable, so the version was burned rather than moved. No 1.2.5 artifact exists for any package.

Upgrading

No configuration or migration changes. No action required for consumers on Node 22 or Node 24.

Library versions

@3ngram/core 0.8.4, @3ngram/db 0.7.3, @3ngram/schema 0.6.3, @3ngram/config 0.2.5, @3ngram/llm 0.2.4.

v1.2.4

view release ↗

Patch release: a Client ID Metadata Document is no longer rejected for advertising a grant type this server does not implement. Together with v1.2.3 this completes CIMD support — the registration mechanism MCP 2026-07-28 designates as primary, replacing deprecated Dynamic Client Registration.

Changes

  • CIMD documents advertising unsupported grants are now accepted. grant_types was validated as an enum of the two grants this server issues, capped at two entries. Any document listing a third grant failed structurally, so the client could not authorize at all — it received a bare 400 invalid_client.

    This locked out real MCP clients. Claude’s metadata document advertises urn:ietf:params:oauth:grant-type:jwt-bearer alongside authorization_code and refresh_token, failing both the enum and the cap.

    grant_types and response_types advertise what a client MAY use (RFC 7591 §2). An authorization server’s obligations for CIMD are to validate that client_id matches the document URL, to validate redirect_uris, and to validate that the structure is valid JSON containing the required fields (client_id, client_name, redirect_uris) — grant_types is not among them. Both fields are now parsed permissively and narrowed to what this server issues.

    Structurally malformed advertisements are still rejected, and the absent-field default is unchanged.

  • grant_types may narrow to empty; response_types may not. The asymmetry is deliberate. /oauth/authorize already rejects a client without authorization_code and reports the precise unsupported_grant_type, so usability stays a policy decision rather than a blanket invalid_document. Nothing downstream consults a client’s advertised response_types, so code must survive narrowing — otherwise a document advertising only token would be issued an authorization code it never advertised support for.

  • No per-element length cap on either field. A cap would reject before narrowing, so one long vendor extension URI would still condemn a document this server can otherwise serve. Input is bounded where it matters: the resolver caps a fetched document at 5 KiB, and the array length is bounded here.

Upgrading

No configuration or migration changes. Clients previously rejected for advertising an unsupported grant can now authorize.

Library versions

@3ngram/schema 0.6.1, @3ngram/core 0.8.2. @3ngram/db 0.7.0, @3ngram/config 0.2.3 — unchanged.

v1.2.3

view release ↗

Patch release: OAuth Client ID Metadata Document (CIMD) client resolution never worked in a deployed build. If you authorize an MCP client that identifies itself by a metadata-document URL rather than dynamic registration, this is the release that makes it work.

Changes

  • Every CIMD client resolution failed under a real ESM runtime. packages/core/src/auth/client-metadata.ts imported ipaddr.js as a namespace (import * as ipaddr). That package is CommonJS with module.exports = ipaddr and no exports map, so Node’s ESM interop — which relies on cjs-module-lexer finding statically scannable assignments — synthesizes no usable named exports. Every member of the namespace was undefined at runtime, so the first call, ipaddr.isValid(...) in resolveHostnameDefault, threw a TypeError.

    resolvePublicTarget catches any resolver throw and relabels it dns_failure, so every CIMD resolution failed — for hostnames and IP literals alike, with no network I/O — and surfaced to the client as a bare 400 invalid_client attributed to DNS. The import is now a default import.

    This was invisible to the test suite: Vitest pre-bundles CommonJS dependencies through Vite’s own interop and synthesizes the named exports, so the module shape under test was never the shape the deployment loaded.

  • New guard: scripts/check-cjs-namespace-imports.mjs. Imports every namespace-imported bare specifier through real Node ESM resolution and fails when a member the source uses is absent from the namespace. Detection is by member use rather than export count (ipaddr.js’s namespace is not empty — the lexer emits a literal module.exports key, so counting exports is a false pass) and covers destructuring as well as property access. Not yet wired into CI; run it with node scripts/check-cjs-namespace-imports.mjs.

Upgrading

No configuration or migration changes. Deployments using CIMD clients should retry authorization after upgrading. Deployments using only dynamic client registration (RFC 7591) are unaffected — that path never went through the broken resolver.

Library versions

@3ngram/core 0.8.1. @3ngram/db 0.7.0, @3ngram/schema 0.6.0, @3ngram/config 0.2.3 — unchanged.

v1.2.2

view release ↗

Patch release: OAuth client resolution. A deployment whose DNS returns IPv4-mapped answers could not authorize any Client ID Metadata Document (CIMD) client at all, and the /oauth/authorize rejection that resulted was invisible in logs. Both are fixed.

Changes

  • CIMD fetches no longer fail closed on IPv4-mapped DNS answers. The SSRF guard’s address-family agreement check compared the resolver-reported family against ipaddr.process(), which unmaps ::ffff:a.b.c.d to its IPv4 form. A resolver reports that answer as family 6, so the unmapped kind (ipv4 → 4) disagreed and a perfectly self-consistent answer was treated as forged: every CIMD document fetch threw unsafe_address before a socket was opened, surfacing to the client as a bare 400 invalid_client. The check now uses ipaddr.parse(), which preserves the wire form.

    The security boundary is unchanged. isPublicClientMetadataAddress() still classifies via process(), so a mapped loopback, private, or otherwise non-public answer (::ffff:127.0.0.1) is still rejected. Both directions are pinned by tests.

  • /oauth/authorize now records why it rejected a request. One structured, content-free line per rejected request (oauth: authorize endpoint) carrying a hashed client_id_prefix and a closed-set reason: not_registered, metadata_* for each CIMD failure class, metadata_not_materialized, unsupported_grant_type, or redirect_uri_mismatch. Previously all of these returned an identical bare 400 invalid_client with nothing written anywhere, so a stale registration and a metadata document that never loaded were indistinguishable in production logs.

    The response is unchanged — still a uniform invalid_client, so there is no enumeration oracle. The reason is diagnostic only and never leaves the log line, and the raw client_id is never written (CIMD identifiers are URLs that may carry a query component — hard rule 6).

Upgrading

No configuration or migration changes. Operators running a CIMD client against a resolver that returns IPv4-mapped addresses will see authorization start working; everyone else sees only the new rejection log line.

Library versions

@3ngram/core 0.8.0 (new optional resolveOAuthClient failure seam; two-argument callers are unaffected). @3ngram/db 0.7.0, @3ngram/schema 0.6.0, @3ngram/config 0.2.3 — unchanged.

v1.2.1

view release ↗

Minor release: the MCP retrieval surface v2 (epic #42). Agents pulling context no longer hit hard walls — every read tool is now bounded, tunable, and pageable, and truncated results are always followable to full content.

(v1.2.0 was tagged but never published: its release image failed the vulnerability-scan gate on a transitive dependency. v1.2.1 is the first published version of this line and additionally forces ip-address to 10.3.1, fixing CVE-2026-69192.)

Changes

  • New MCP tool get_memories — batched full-content read by id (up to 20 ids, per-item maxContentChars). A truncated: true search or handoff hit is no longer a dead end. Unknown and cross-tenant ids land in notFound as data, never an error.
  • Briefing/handoff bounds are caller-tunablesectionLimit (up to 100, previously a fixed cap of 25), sections selection on briefing (compute only what you need), hasMore + exact count per briefing section, and exact counts + per-section truncated flags on handoff.
  • Search filters v2memoryTypes[] (multi-type OR) and recordedAfter/recordedBefore range filters, on MCP search and the REST list endpoint alike. Filters narrow candidates before ranking fusion; the ranking itself is unchanged.
  • Search pagination — an opaque cursor with frozen-pool ordering (no duplicates or skips under concurrent writes) plus a projection: 'compact' mode that omits hit content for cheap broad scans; scan compact, then get_memories the interesting ids.
  • scope_project selector — scope AND project together, with an includeUnscoped: true opt-in that surfaces NULL-project memories in project-filtered briefings and handoffs.
  • Per-user retrieval-scope policyconfigure_scope gains set_retrieval_default: mode default fills unscoped reads with your default scope and echoes appliedScope (never silent); require rejects unscoped reads with a typed error naming the registered scopes; off preserves shipped behavior exactly.
  • Stale-candidate allowlist — briefing staleCandidates now only proposes reviewable types (decision, preference, blocker, fact) instead of every aging memory.
  • Recorded-range hardening — both transports reject inverted ranges and sub-millisecond precision at the schema boundary.

Upgrade notes

  • New database migration 0030_user_retrieval_policy. Self-hosters: run the migrations step (docker compose … run --rm migrations) before starting the new server, as in the standard upgrade flow.
  • All shipped tool contracts are unchanged (byte-stable): new fields and selector variants are additive and optional.

Release artifacts

  • @3ngram/server@1.2.1, @3ngram/sdk@1.2.1, and 3ngram@1.2.1 on npm
  • @3ngram/db@0.7.0, @3ngram/core@0.7.0, and @3ngram/schema@0.6.0 on npm
  • @3ngram/config@0.2.3 and @3ngram/llm@0.2.2 on npm
  • ghcr.io/b3dmar/3ngram:1.2.1 for Linux amd64 and arm64
  • BuildKit SBOM plus provenance and GitHub-signed SLSA attestations

The hosted 3ngram dashboard and cloud-operations code are proprietary and are not included in this repository.

v1.1.3

view release ↗

Patch release: fix the database migrator’s role-provisioning step.

Changes

  • runMigrations() now executes provision-roles.sql over the Postgres simple query protocol instead of the extended protocol, so its DO $$…$$ block and multi-statement body run correctly (previously the provisioning step failed).
  • The migrator substitutes the runtime role from RUNTIME_DB_ROLE (default app_user), so a deployment running as a re-provisioned NOBYPASSRLS role is granted correctly.
  • ALTER ROLE … NOBYPASSRLS in provisioning is now tolerant of managed Postgres (e.g. Neon) where the provisioning role lacks privilege to change BYPASSRLS; CREATE ROLE … NOBYPASSRLS already sets it.

Release artifacts

  • @3ngram/server@1.1.3, @3ngram/sdk@1.1.3, and 3ngram@1.1.3 on npm
  • @3ngram/db@0.6.2 and @3ngram/core@0.6.3 on npm
  • ghcr.io/b3dmar/3ngram:1.1.3 for Linux amd64 and arm64
  • BuildKit SBOM plus provenance and GitHub-signed SLSA attestations

The hosted 3ngram dashboard and cloud-operations code are proprietary and are not included in this repository.

v1.1.2

view release ↗

Patch release: make the production DATABASE_URL runtime-role check configurable, completing the pair with the v1.1.1 RLS-guard change.

Changes

  • The production DATABASE_URL validation now honors the RUNTIME_DB_ROLE environment variable (defaulting to app_user) instead of hardcoding app_user. A deployment whose runtime connects as a differently-named NOBYPASSRLS role passes env validation. No behavior change when RUNTIME_DB_ROLE is unset.

Release artifacts

  • @3ngram/server@1.1.2, @3ngram/sdk@1.1.2, and 3ngram@1.1.2 on npm
  • @3ngram/core@0.6.2 and @3ngram/config@0.2.3 on npm
  • ghcr.io/b3dmar/3ngram:1.1.2 for Linux amd64 and arm64
  • BuildKit SBOM plus provenance and GitHub-signed SLSA attestations

The hosted 3ngram dashboard and cloud-operations code are proprietary and are not included in this repository. The MCP server, REST API, SDK, CLI, and memory engine do not depend on that dashboard.

v1.1.1

view release ↗

Patch release: make the runtime row-level-security guard’s expected database role configurable.

Changes

  • The runtime RLS readiness guard (GET /ready) now reads the expected current_user from the RUNTIME_DB_ROLE environment variable, defaulting to app_user. A deployment whose runtime connects as a differently-named NOBYPASSRLS role now passes readiness instead of failing on a hardcoded role name. No behavior change when RUNTIME_DB_ROLE is unset.

Release artifacts

  • @3ngram/server@1.1.1, @3ngram/sdk@1.1.1, and 3ngram@1.1.1 on npm
  • @3ngram/core@0.6.1 and @3ngram/db@0.6.1 on npm
  • ghcr.io/b3dmar/3ngram:1.1.1 for Linux amd64 and arm64
  • BuildKit SBOM plus provenance and GitHub-signed SLSA attestations

The hosted 3ngram dashboard and cloud-operations code are proprietary and are not included in this repository. The MCP server, REST API, SDK, CLI, and memory engine do not depend on that dashboard.

v1.1.0

view release ↗

Feature and hardening release of 3ngram: persistent, typed memory for AI agents. It adds Client ID Metadata Document (CIMD) OAuth, a single stateless MCP handler spanning legacy and current protocol clients, and a defense-in-depth pass over tenant isolation.

Tenant-isolation hardening

  • FORCE ROW LEVEL SECURITY is now set on the tenant-data tables, so isolation no longer depends on the runtime never connecting as a table owner.
  • A runtime, fail-closed guard verifies at readiness (GET /ready) that the connected role cannot bypass RLS and that FORCE is in effect; a misprovisioned database is held out of rotation instead of serving cross-tenant reads.
  • Every memory-domain read and scope mutation now also carries an explicit caller-bound user_id predicate as a second layer alongside RLS. Result sets are unchanged while RLS is in force.
  • audit_log is brought under row-level security, and the runtime role’s NOBYPASSRLS attribute is re-asserted on every provisioning run.

OAuth and MCP

  • OAuth Client ID Metadata Document (CIMD) resolution with SSRF-safe fetching and bounded HTTP caching.
  • A single stateless MCP SDK v2 handler serves both legacy and 2026-07-28 clients from one code path.
  • Cache and pre-parser header observability for the MCP surface.

Release artifacts

  • @3ngram/server@1.1.0, @3ngram/worker@1.1.0, @3ngram/sdk@1.1.0, and 3ngram@1.1.0 on npm
  • @3ngram/core@0.6.0, @3ngram/db@0.6.0, @3ngram/schema@0.5.0, and @3ngram/config@0.2.2 on npm
  • ghcr.io/b3dmar/3ngram:1.1.0 for Linux amd64 and arm64
  • BuildKit SBOM plus provenance and GitHub-signed SLSA attestations

The hosted 3ngram dashboard and cloud-operations code are proprietary and are not included in this repository. The MCP server, REST API, SDK, CLI, and memory engine do not depend on that dashboard.

v1.0.2

view release ↗

Security-hardened public release of 3ngram: persistent, typed memory for AI agents. This patch is intended as the first complete cross-package release after the v1.0.1 publication run was halted during code-scanning review.

Security hardening

  • A coarse per-IP rate limit now covers every non-health HTTP surface before body parsing, backed by Redis across replicas and in-memory for self-hosting.
  • Login, signup, OAuth, MCP, and API-key traffic retain their narrower dedicated rate-limit buckets in addition to the coarse edge limit.
  • Core, LLM, and SDK URL normalization now runs in linear time for arbitrary trailing-slash input.
  • Documentation generators, evaluation tests, and CI permissions were tightened in response to the public CodeQL baseline.

Release artifacts

  • @3ngram/server@1.0.2, @3ngram/sdk@1.0.2, and 3ngram@1.0.2 on npm
  • @3ngram/core@0.5.1 and @3ngram/llm@0.2.2 on npm
  • The existing exact config, schema, and database packages that complete the server dependency closure
  • ghcr.io/b3dmar/3ngram:1.0.2 for Linux amd64 and arm64
  • BuildKit SBOM plus provenance and GitHub-signed SLSA attestations

The hosted 3ngram dashboard and cloud-operations code are proprietary and are not included in this repository. The MCP server, REST API, SDK, CLI, and memory engine do not depend on that dashboard.

v1.0.1

view release ↗

The first public artifact release of 3ngram: persistent, typed memory for AI agents. This patch supersedes the unpublished v1.0.0 release attempt.

Highlights

  • 10 MCP tools and 2 prompts over Streamable HTTP
  • REST API and typed TypeScript SDK
  • Append-and-supersede memory history with bi-temporal facts
  • Postgres + pgvector retrieval with a blocking golden-set quality gate
  • Apache-2.0 server, engine, SDK, and CLI with a self-hosted Compose path
  • A smaller production server image with the unused npm toolchain removed

Release artifacts

  • Public @3ngram/* libraries and the 3ngram CLI on npm
  • ghcr.io/b3dmar/3ngram:1.0.1 for Linux amd64 and arm64
  • BuildKit SBOM and provenance attached to the OCI image
  • GitHub-signed SLSA provenance, verifiable with gh attestation verify

The hosted 3ngram dashboard and cloud-operations code are proprietary and are not included in this repository. The MCP server, REST API, SDK, CLI, and memory engine do not depend on that dashboard.

See the container image guide for immutable pulls and attestation verification, and the self-host guide for deployment.

v1.0.0

view release ↗

The first public release of 3ngram: persistent, typed memory for AI agents.

Highlights

  • 10 MCP tools and 2 prompts over Streamable HTTP
  • REST API and typed TypeScript SDK
  • Append-and-supersede memory history with bi-temporal facts
  • Postgres + pgvector retrieval with a blocking golden-set quality gate
  • Apache-2.0 server, engine, SDK, and CLI with a self-hosted Compose path

Release artifacts

  • Public @3ngram/* libraries and the 3ngram CLI on npm
  • ghcr.io/b3dmar/3ngram:1.0.0 for Linux amd64 and arm64
  • BuildKit SBOM and provenance attached to the OCI image
  • GitHub-signed SLSA provenance, verifiable with gh attestation verify

The hosted 3ngram dashboard and cloud-operations code are proprietary and are not included in this repository. The MCP server, REST API, SDK, CLI, and memory engine do not depend on that dashboard.

See the container image guide for immutable pulls and attestation verification, and the self-host guide for deployment.

start with the latest 3ngram.

free · no card · persistent typed memory for the AI client you connect.

get started free ↵