> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xysq.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Thread-level memory

> Your conversation's turns, stored server-side as they happen — instantly readable back, automatically promoted into long-term memory.

A live conversation needs two memories: the turns themselves, right now, in
order — and the durable knowledge they turn into. `client.threads` gives you
both from one call.

```python theme={"dark"}
from xysq import Xysq

with Xysq() as client:
    vault = client.vaults.create("Support Bot")
    tid = "ticket-4471"                    # your conversation id

    turn = client.threads.append(vault.vault_id, tid, "user", "my order is late")
    print(turn.seq)                        # 1 -- your write, verified

    client.threads.append(vault.vault_id, tid, "assistant",
                          "Refunded. You'll see it in 3 days.")

    # readable back immediately -- no processing delay, no model involved
    window = client.threads.read(vault.vault_id, tid, last_n=12)
    for t in window.turns:                 # oldest first
        print(t.seq, t.role, t.content)

    # ...and later, from ANY conversation, the durable knowledge comes back
    hits = client.vaults.pull(vault.vault_id, "what happened with the late order")
```

That's the whole integration. Appended turns are promoted into the vault's
long-term memory for you — automatically every few turns, on `flush()`, and
idle threads are swept in — where they're distilled and become
[pull](/sdk/vaults#pull)-able across every conversation.

## The contract

* **`append` returns the turn's `seq`.** The write is verifiable, not
  fire-and-forget. Retries are safe: each logical turn carries an idempotency
  key, so a timeout plus a retry can never store the same turn twice. If
  *your* code retries a failed call, pass the same `turn_key`.
* **`read` is instant and always bounded** — default 50 turns, max 500, or a
  `token_budget`. A cut window sets `truncated: true`; it never masquerades
  as a short conversation. `window.flushed_through` is the last turn already
  promoted to long-term memory.
* **`list` recovers your thread ids after a restart.** History is server
  state, not process state — a fresh client on the same `thread_id` resumes
  the conversation.
* **`flush` promotes now; `clear` ends a conversation.** Clear flushes first
  (nothing is ever silently discarded), wipes the working window, and never
  reuses a sequence number.
* **Roles are `user` and `assistant`.** Map anything else (`system`, `tool`)
  before appending, or drop it.
* **`thread_id` is yours**: any stable id up to 200 chars of
  `[A-Za-z0-9._:-]`, usually your own conversation id.

`XysqAgent` is built on exactly this — if you want the loop run for you too,
see [the agent](/sdk/agent).

## Bring your own transcript store

If you already keep the conversation yourself (your own Postgres, your own
context window) and only want the **long-term** half, skip `threads` and push
turns directly. Two rules make it work well:

**Group the conversation with a `session_id`** — a stable id per
conversation, so repeated pushes append to one document instead of
fragmenting into many:

```python theme={"dark"}
client.vaults.push(
    vault.vault_id,
    "user: my order is late\nagent: I've refunded it.",
    metadata={"session_id": "conv-8842", "format": "turns"},
)
```

**Push non-overlapping deltas** — the new turns only, never the conversation
so far. An exact re-send of an identical payload is deduplicated; a
*cumulative* push is not, and stores repeated content.

Three sharp edges on this manual path (none apply to `client.threads`, which
formats turns for you):

* `format: "turns"` tells us to read the content as a dialogue. Turns are
  `user:` and `agent:` lines — those two prefixes exactly. Relabel
  `Human:`/`Assistant:` or anything else before pushing, or the payload is
  treated as one block of prose.
* **Anything before the first `user:`/`agent:` line is not stored.** Put
  dates or session context inside a turn, or push them separately.
* **Only `user:` and `agent:` are recognised.** A `system:` line is not a
  turn — map or filter it explicitly, or that content is silently absent
  later.

One namespace rule connects the two paths: `session_id` values starting with
`thread:` belong to the checkpointer, and a push claiming one is rejected —
so your manual pushes and a thread can never write into the same document.

## Which one am I?

* Building an agent and happy for xysq to hold the conversation →
  **`client.threads`**. Working memory and long-term memory from one call.
* Already have a transcript store you trust (your own DB, your own window
  assembly) → **manual pushes** with `session_id` + `format: "turns"` +
  non-overlapping deltas. You keep your recent-turn layer; xysq carries
  everything older and everything cross-conversation.

Either way, recall is the same: `pull` returns ranked hits from across the
vault, current conversation and earlier ones together. Content becomes
pull-able once background processing finishes — the thread window is the
thing that's instant.
