Skip to main content
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.
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-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.

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:
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.