Skip to content
Public beta preview — you're seeing the sneak peek · official launch soon

State & memory

read as .md

Guuey gives agents and MCP servers a small, deliberate set of persistence primitives. Each is scoped tightly — per user, per app — and each has hard caps. The limits are the point: your server stays stateless from its own point of view, and your users keep ownership of their data.

Every conversation with a hosted agent is persisted automatically by the platform. Users reconnect and the thread is there. Don’t store chat history in any of the primitives below — that job is already done.

Keep your memory when you self-host: @guuey/threads

Section titled “Keep your memory when you self-host: @guuey/threads”

The same session model is a public contract — @guuey/threads — so an agent you run yourself gets the same rehydration. Two bindings ship in the box: in-memory (dev, tests) and hosted, which points your agent at Guuey’s thread API and reads and writes the very rows Guuey’s hosted platform does. Eject the code; keep the memory.

import { HttpThreadPersistence, ThreadStore } from "@guuey/threads";
const port = new HttpThreadPersistence({
baseUrl: "https://api.us-east-1.guuey.com",
appId: "<your app id>",
token: endUserToken, // the end-user's own token, per request
});
const { userId, region } = await port.scope();
const store = new ThreadStore(port);

The hosted binding is scoped to one app and one signed-in end-user. The token is one your app’s identity issuer minted — your own IdP, or Guuey’s per-app issuer through @guuey/widget-auth — so this needs your app in identified mode (userAuthMode: "byo"; see Embed & share). Guuey verifies the token against that issuer and confines every read and write to that user’s threads in that app: an ejected agent can never see another user’s conversation, and a token minted for one app is refused at another. Anonymous guests and Portal sign-ins are persisted on-platform only.

Or bring your own store: implement the ThreadPersistencePort interface and run the contract suite from @guuey/threads/testing — the same suite Guuey runs against its own DynamoDB binding and against the hosted API.

A key-value store scoped to (user, MCP server). It’s built for the state most MCP servers actually need: idempotency tokens, rate-limit counters, OAuth nonces, small per-user preferences.

import { createGuueyState } from "@guuey/state";
const kv = createGuueyState({
context: { userId: "u_abc", mcpId: "mcp_xyz" },
});
await kv.set("user-prefs", { theme: "dark" }, { ttl: 60 * 60 * 24 * 7 });
const prefs = await kv.get<{ theme: string }>("user-prefs");

In production you don’t pass the context by hand — it’s derived from the signed request Guuey sends your server. See the package README for the per-request wiring.

The caps are enforced everywhere, and they are the product:

  • 1 MiB per (user, server) scope; 64 KiB per value.
  • Every write needs a TTL — no permanent keys; 90 days max.
  • KV only: no queries, joins, or transactions.
  • Strict isolation: one MCP server can never read another server’s keys, even for the same user.

Without a hosted binding in your environment, the same API runs in-memory — exactly right for tests and guuey dev, non-durable by design. The reverse never happens silently: when a hosted binding is configured (bindingUrl or GUUEY_KV_URL) but no token resolves, createGuueyState throws InvalidContextError instead of falling back to in-memory — misconfiguration fails loudly, never as invisible data loss. If you outgrow the caps, that’s the signal to move the data to a real backend and call it from your MCP server.

Every hosted agent runs with three directories bound in, used with plain node:fs — the @guuey/fs helpers just tell you where they are:

Helper What it is Lifetime
homeDir() Read-write, per (app, user) Durable where enabled — survives restarts and new sessions (see the note below)
appDir() Read-only files that ship with the app Same for every user
sessionDir() Read-write scratch (the working directory) This session only

The durable home is what makes “my agent remembers me” work: where durable storage is enabled for your app’s environment, files written for a signed-in user today are there next week, from a fresh pod. Until the rollout reaches your environment — production is not there yet, see the note at the top of this page — the home is still real and writable, but pod-local: like sessionDir(), it does not survive a restart. The convention is a memories/MEMORY.md file in the home directory — the platform reads it before each turn and injects it into the model’s context for you, identically on all three frameworks. Your agent can also save memories mid-conversation; the platform tells it how.

The home has a hard cap too: each (app, user) home carries a size quota set by the app owner’s plan — 100 MiB on Free (trial), 1 GiB on Starter, 5 GiB on Pro, 20 GiB on Scale — and the filesystem itself rejects writes past it. The larger “included app storage” figures on Plans & billing are the whole app’s aggregate allowance, not the per-user home quota. How bytes above the app’s allowance meter — and why a capped app gets a disk-full error instead of a bill — is in How billing works.

Anonymous guests get a real, writable home too — but it’s deliberately ephemeral. Guests never accumulate durable storage; don’t build a feature that assumes otherwise.

A consent-gated user profile that can follow a user between apps — rolling out now.

  • Your app opts in by declaring profileAccess: "read" or "read-write" in guuey.json. No declaration, no access, and no prompt — ever.
  • The first time your app wants the profile in a conversation, the user sees a consent card in chat and chooses: always, only this conversation, or deny. Grants can be reviewed and revoked later from their Guuey settings.
  • Each app writes only its own section of the profile — one app structurally cannot touch another’s contribution.
  • Signed-in users only, and every unclear case fails closed: without consent the profile is simply absent and the conversation continues normally.

State is visible to the people it’s about: users can see and delete what an app has stored about them. Design your server so that a user clearing their data is a fresh start, never an error.

The builder side of that promise is in the CLI. For a hosted MCP server’s per-user KV state:

Terminal window
guuey mcp state list --server <id> # per-user stored-state usage
guuey mcp state export --user <id> --server <id> # one user's entries as JSON (portability)
guuey mcp state wipe --user <id> --server <id> # irreversibly delete one user's entries (erasure)

Each verb also takes --colocated <appId>/<name> to target a colocated MCP server instead of --server. wipe confirms interactively (--yes to skip) and refuses outright on a non-interactive session without it.

For apps in identified mode (userAuthMode: "byo"), guuey apps byo-user erase <appId> --sub <sub> erases one end-user’s app data in full: it enqueues the durable-home memory wipe and synchronously deletes their threads and sessions for the app. It’s idempotent, and --status polls the wipe state instead of enqueuing.