State & memory
read as.mdGuuey 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.
Conversation history
Section titled “Conversation history”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.
Small state: @guuey/state
Section titled “Small state: @guuey/state”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.
A home directory per user
Section titled “A home directory per user”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.
Cross-app profile
Section titled “Cross-app profile”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"inguuey.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.
Your users own their data
Section titled “Your users own their data”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.
Operating on user data
Section titled “Operating on user data”The builder side of that promise is in the CLI. For a hosted MCP server’s per-user KV state:
guuey mcp state list --server <id> # per-user stored-state usageguuey 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.