Core concepts
Context and memory
Four different things get called "memory", and they behave nothing alike. Separating them is the difference between an assistant that feels continuous and one that leaks.
The four
| Lives for | Scoped to | Where it comes from | |
|---|---|---|---|
| Conversation history | 7 days | Tenant + whoever is asking + session | Turns you made |
| Durable preferences | Until overwritten | Tenant + whoever is asking | Extracted from what people said |
| Retrieved knowledge | Until you delete or replace it | Tenant, or global | Documents you upload, and Preview Mode's crawl |
| Pre-turn context | Seconds to minutes | Tenant + scope | Assembled per turn, cached |
Tool results are not on this list. They exist for one turn, inside the model's context, and are never stored.
Whose memory a turn uses
Every turn has one owner, and its history and preferences are keyed by that owner and nothing else:
| Who is asking | How Tether knows | Remembered for |
|---|---|---|
| A verified customer | A customer token your server signed | That customer, across every conversation |
| An unverified visitor | Nothing but the conversation Tether issued to them | That one conversation |
| A member of staff | X-Tether-User-Id from your backend, with no customer token | That user, as before |
A turn with both a customer token and X-Tether-User-Id is the customer's. What one guest says,
"reply in English" included, never reaches another guest of the same business, verified or not.
meta.explain.memory shows which owner a turn had, how many prior messages it loaded and which
preference keys (see Chat).
Conversation history
Echo session_id and the thread continues. The last 10 messages within a 7-day window
are replayed, scoped by tenant, owner and session — all of which must match.
A guest's conversation is continued only by the guest it was opened for. A customer's belongs to
that customer, whether Tether issued the id or your host chose it: the first turn binds it, and a
turn for anyone else naming it is refused. An unverified visitor's exists only if Tether issued it
to an unverified turn (omit sessionId and use the session_id that comes back): an id Tether
never issued is refused, because nothing else tells one anonymous visitor from another. A
conversation never changes owner, so a visitor who signs in starts a new one. Refusals are 403 session_not_yours, before any history is read:
error.reason | The turn named |
|---|---|
other_customer | Another customer's conversation |
customer_session | A customer's conversation, without that customer's token |
anonymous_session | A conversation an unverified visitor opened, with a customer token |
not_issued | A conversation Tether never issued, without a customer token |
Staff turns are not bound: your backend may choose their ids, as before, because their history is keyed by the user id.
{ "prompt": "which one is oldest?", "sessionId": "0f2c8b41-…" }
The window is AI:Policy:HistoryWindowDays. The 10-message cap is not configurable; it is
a token-budget decision, and a longer thread does not make answers better in proportion to what
it costs on every subsequent turn.
Where the provider supports server-side conversation state, Tether also threads response_id
so a turn sends only what is new. That is an optimization, not the source of truth — the
replayed rows above are.
Durable preferences
A preference is a small, stable fact about how one person wants to be answered.
preferred_units lbs
response_style concise
default_scope 9
They are extracted from conversation, stored one row per tenant + owner + key (updates overwrite, never append), and injected into the system prompt on later turns. A remembered "reply in X" also decides the reply language: a verified customer's follows them into their next conversation, an unverified visitor's lasts for that conversation.
Two thresholds govern them, both defaulting to 0.7:
| Setting | Effect |
|---|---|
MinPreferencePersistConfidence | Below this, an extracted preference is discarded rather than stored |
MinPreferenceInjectConfidence | Below this, a stored preference is not injected into the prompt |
Extraction runs in two passes. A regex extractor is always on, is free, and covers the common
phrasings. An optional LLM extractor (LlmPreferenceExtractionEnabled, off by default) adds one
model call per turn and catches the rest.
Extraction is best-effort by construction: a failure is logged and swallowed, because a memory subsystem must never take an answer down with it.
Retrieved knowledge
Documents you upload with POST /v1/knowledge, and pages Preview Mode
crawls, are chunked, embedded, and stored as pgvector rows. On a turn, the prompt is embedded once
and the nearest chunks are retrieved by cosine similarity, with a bonus for shared words. When no
embedding provider is configured, or the call fails, retrieval is by keyword alone, and it reads
Georgian in both scripts: a question typed in Latin letters is searched in Georgian script, and a
word matches its other case endings.
| Setting | Default | Notes |
|---|---|---|
KnowledgeRetrievalEnabled | on in the shipped configuration | Requires pgvector on the database |
RetrievalTopK | 5 | Chunks per retrieval call |
MinCosineSimilarity | 0.25 | Tuned low deliberately — real matches score 0.25–0.42 with the default embedding model |
EmbeddingModel | text-embedding-3-small | 1536 dimensions, matching the column. Changing it means re-embedding everything |
IngestionJobEnabled | off | Turning it on starts paying embedding costs |
A chunk is either global — product-wide documentation shared by every tenant — or scoped to one tenant. There is no in-between, and a tenant-scoped chunk is never retrieved for another tenant.
Retrieval is what makes the assistant able to answer "how do I do X in your product", which no
tool can answer. It is not a substitute for tools: a document says what is supposed to happen,
and a tool says what did. A turn that draws on your documents is still offered every tool, and is
not flagged preview; its meta.knowledge names the documents.
When IncludeSourcesInMeta is on, the chunks that informed an answer come back in
meta.sources, which is what lets you show a citation.
Pre-turn context
Before the model is called, Tether can assemble a snapshot of the tenant's world — cached, with a per-section TTL and a bounded concurrent build — and hand it to the model as grounding it did not have to spend a tool call on.
What Tether stores, and what it never sees
Stored, per tenant: conversations, staged actions and their execution records, the spend ledger, request telemetry, extracted preferences, ingested knowledge chunks, the tool catalog, and audit rows.
No table for: your orders, customers, inventory, invoices — any business record. Tether has no schema for them, never receives a bulk copy, and cannot query your database. It reaches your data only by calling an endpoint you control.
But "never stored" would be wrong, and used to be what this page said. Your business data passes through a turn, and parts of it are kept:
| What | Where | Why it is kept |
|---|---|---|
| Conversation text | Chat history | The next turn needs the previous ones |
| Tool arguments | Staged actions | An approver cannot approve a write they cannot read |
| Execution results | Action executions | The record of what a confirmed write actually did |
| Extracted preferences | Memory | Stated preferences, kept on purpose |
| Ingested knowledge | Knowledge chunks | Retrieval needs something to retrieve |
A staged refund names an order and an amount. A conversation about a customer contains that customer. So the honest answer to "what would a breach of Tether expose" is: conversations, the arguments of writes that were staged, the results of writes that ran, and anything a tool returned that was quoted back — not a copy of your database, and not nothing.
That distinction is the reason the service can be operated by someone other than you, and it is worth stating precisely rather than generously.
Turning it all off
{ "prompt": "…", "includeContext": false }
Skips pre-turn context assembly for that turn. History, preferences and retrieval are governed by configuration rather than per request.