Conversation History and Framework State, Kept Apart From Memory
New /v1/history and /v1/state endpoints keep transcripts and framework state exactly as written, in stores of their own, so they never show up in memory search.
Published on 5 min read

On this page
A chat framework that replays history needs every turn back exactly as it was written, including the assistant replying Noted. A memory store does the opposite job: every write goes through extraction, and only the durable facts it finds are stored. Ask one store to do both and it does one of them badly.
Today we are adding two stores beside memory: conversation history at /v1/history/* and framework state at /v1/state/*. The Python SDK supports both from memorysync 1.10.0, and the TypeScript SDK has the same methods in camelCase.
Three stores, three jobs
The running example is Acme's travel assistant and one of its users, user_42, chatting in a session called thread-42.
| Store | What it holds | How you read it |
|---|---|---|
| Memories | Durable facts extracted from what you send | Search ranked by meaning, with query and recall |
| Conversation history | The turns of a session, exactly as given | list_history, oldest first |
| Framework state | Namespaced key/value values a framework keeps between runs | get_state, list_state and search_state |
History and state are never memories. Nothing written to them appears in memory search, memory lists or recall, so a replayed transcript cannot crowd the facts out of a prompt, and a framework's saved record cannot be mistaken for something the user said.
The stores still connect where it helps. User turns appended to history can be sent to extraction, and the facts found become ordinary memories of the same user.
Conversation history
Append turns to a session, read them back in order, and delete them when the conversation is gone. This is the example from the SDK README:
ms.append_history(tenant_id="acme",user_id="user_42",session_id="thread-42",source="my-app",turns=[{"role": "user", "content": "I prefer window seats.", "seq": 0},{"role": "assistant", "content": "Noted.", "seq": 1},],)history = ms.list_history(tenant_id="acme", user_id="user_42", session_id="thread-42")for turn in history["items"]: # oldest firstprint(turn["role"], turn["content"])ms.delete_history(tenant_id="acme", user_id="user_42", session_id="thread-42")
How it behaves:
- Turns come back exactly as given, oldest first. When any turn in the session has a
seq, the turns are ordered by it. One call appends up to 100 turns. - User turns go to extraction by default. With
extract=True, the default, the user turns are also sent to fact extraction, and the facts found become memories of the same user, carrying thesession_id.I prefer window seats.is kept word for word in history, and the preference it states also becomes a memory ofuser_42. Passextract=Falseto keep the transcript only. - The assistant turn stays in history only.
Noted.is part of the transcript the framework replays, and it never becomes a memory.
Framework state
Some frameworks keep records between runs, such as a store item or a saved record about the user. Framework state holds those as namespaced key/value pairs. Again from the README:
ns = ["memories", "user_42"]ms.put_state(tenant_id="acme", user_id="user_42", namespace=ns, key="diet",value={"diet": "vegan"}, source="my-app")item = ms.get_state(tenant_id="acme", user_id="user_42", namespace=ns,key="diet", source="my-app")["item"]hits = ms.search_state(tenant_id="acme", user_id="user_42", query="what does the user eat",namespace_prefix=["memories"], source="my-app")ms.delete_state(tenant_id="acme", user_id="user_42", namespace=ns, keys=["diet"],source="my-app")
How it behaves:
- A put replaces the key's value. Putting the identical value again returns
created: False, so a retried put is safe. - `namespace` is a list of 1 to 20 strings, and `value` must be a JSON object.
get_statereturnsitem: Nonewhen the key has no value. - `search_state` ranks values against a query. It uses cosine similarity when the value was embedded, which is the default, and token overlap when you passed
index=False. - State is not extracted unless you ask.
extract=Falseis the default for a put. Set it toTrueto also extract durable facts from the value's text. - Pass the same `source` on every call. Rows are scoped by
source, and the state reads default to"api", so a value put withsource="my-app"is invisible to a read that leavessourceout.
Retries and deletes
Both stores are built to be called from code that retries:
- A history turn retried with the same
seq, role and content, or the sameidempotency_key, is not stored twice. The stored turn comes back withcreated: False. A turn with neither is appended every time, so give your turns aseqor a key. delete_historytakes exactly one ofsession_idorids. Passing both, or neither, is refused before any request is sent.delete_history(..., delete_derived_facts=True)also deletes the memories extracted from those turns, and their ids come back indeleted_fact_ids. Without it, the facts stay after the transcript is gone.- Omitting
keysondelete_statedeletes the whole namespace, and an empty list deletes nothing.delete_statetakesdelete_derived_facts=Trueas well.
# Remove the session and everything extracted from itresult = ms.delete_history(tenant_id="acme",user_id="user_42",session_id="thread-42",delete_derived_facts=True,)print(result["deleted_fact_ids"])
Decide which behaviour you want before a user asks to clear a conversation. Clearing the transcript and forgetting what the user said in it are two different requests, and the flag is how you tell them apart.
What each call costs
- An append counts one add, however many turns it carries, and a put counts one add. A retry of something already stored is not counted again.
- Reads count one retrieval each, whatever they return.
- History and state deletes are free.
- Over the plan limit the calls return normally and store nothing. Writes answer
status: "skipped"and reads come back empty.
So a framework that appends a whole exchange in one call pays one add for it, while one that appends turn by turn pays per call. Batch turns where the framework allows it.
The same calls in TypeScript
The TypeScript SDK takes camelCase requests and returns the server's JSON as it is, so response fields stay snake_case:
await ms.appendHistory({tenantId: "acme",userId: "user_42",sessionId: "thread-42",source: "my-app",turns: [{ role: "user", content: "I prefer window seats.", seq: 0 },{ role: "assistant", content: "Noted.", seq: 1 },],});const history = await ms.listHistory({ tenantId: "acme", userId: "user_42", sessionId: "thread-42" });for (const turn of history.items) console.log(turn.role, turn.content); // oldest first
Framework adapters are the natural users of both stores, and Memory for Every Agent Stack lists the official ones. If you write your own adapter, put the transcript in history, put framework records in state, and let extraction turn the user's words into memories.
Read the reference
Every field, limit and response shape for both stores is in Conversation History & State.

