Tether
DocsGitHub

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 forScoped toWhere it comes from
Conversation history7 daysTenant + whoever is asking + sessionTurns you made
Durable preferencesUntil overwrittenTenant + whoever is askingExtracted from what people said
Retrieved knowledgeUntil you delete or replace itTenant, or globalDocuments you upload, and Preview Mode's crawl
Pre-turn contextSeconds to minutesTenant + scopeAssembled 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 askingHow Tether knowsRemembered for
A verified customerA customer token your server signedThat customer, across every conversation
An unverified visitorNothing but the conversation Tether issued to themThat one conversation
A member of staffX-Tether-User-Id from your backend, with no customer tokenThat 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.reasonThe turn named
other_customerAnother customer's conversation
customer_sessionA customer's conversation, without that customer's token
anonymous_sessionA conversation an unverified visitor opened, with a customer token
not_issuedA 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:

SettingEffect
MinPreferencePersistConfidenceBelow this, an extracted preference is discarded rather than stored
MinPreferenceInjectConfidenceBelow 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.

SettingDefaultNotes
KnowledgeRetrievalEnabledon in the shipped configurationRequires pgvector on the database
RetrievalTopK5Chunks per retrieval call
MinCosineSimilarity0.25Tuned low deliberately — real matches score 0.25–0.42 with the default embedding model
EmbeddingModeltext-embedding-3-small1536 dimensions, matching the column. Changing it means re-embedding everything
IngestionJobEnabledoffTurning 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:

WhatWhereWhy it is kept
Conversation textChat historyThe next turn needs the previous ones
Tool argumentsStaged actionsAn approver cannot approve a write they cannot read
Execution resultsAction executionsThe record of what a confirmed write actually did
Extracted preferencesMemoryStated preferences, kept on purpose
Ingested knowledgeKnowledge chunksRetrieval 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.