# Conversation continuity

Page: https://docs.sonecentral.com/en/sohbet-surekliligi
Markdown: https://docs.sonecentral.com/en/sohbet-surekliligi.md

> Keep anonymous chats across refreshes, understand conversation lookup and retry messages safely.

API visitors have no authenticated account or identity. Keep **the conversation ID** and **the displayed messages** in their browser so the chat can continue.

## The authoritative record is on the server

Sonecentral stores each conversation and the assistant reads its history there. Submitted history is used only to:

- Identify new messages.
- Find a conversation when no ID was supplied.
- Recognize retries.
- Initialize a new conversation if none exists.

Changing submitted history does not change the assistant's memory. It sees the same conversation your team sees in Messages.

## Conversation lookup

Sonecentral uses the first matching method in this order:

1. **Conversation ID**, prefixed with `conv_`.
   - Chat Completions: body `conversation_id`, body `sone.conversation_id`, `X-Sone-Conversation-Id`, `metadata.conversation_id`, then `sone.conversation_id` on the most recent assistant message returned unchanged in history. The first non-empty value wins.
   - Responses: `conversation`, then `X-Sone-Conversation-Id`.
   - Vercel AI SDK: body `conversationId`, `X-Sone-Conversation-Id`, then a returned assistant message's `metadata.conversationId`.
2. **Responses `previous_response_id`.**
3. **Responses previous output IDs:** `item_reference` items or returned assistant messages with `msg_…` IDs.
4. **Vercel AI SDK's `useChat` ID**, subject to the four conditions below.
5. **`user` or `safety_identifier`**, server keys only; Vercel AI SDK uses body `user`. The same value and assistant find the same conversation.
6. **History fingerprint.** If history starts at the conversation's first message, Sonecentral records a fingerprint after the turn. Matching history finds the conversation subject to the four conditions below. Requests sending only a new message with an explicit conversation ID do not record a fingerprint.
7. **First-message retry.** The same `Idempotency-Key`, or AI SDK message ID, can find the first message's conversation for ten minutes if it is still the latest visitor message there. Publishable keys also require the same network address and, for AI SDK, the same `useChat` ID. See [Retries](#tekrar-denemek).
8. Otherwise, **a new conversation** is created. It imports the last 40 history messages, up to 4,000 characters each. Publishable keys import only visitor messages; assistant messages can be altered by the browser and are not imported.

Sonecentral issues IDs used by the first three methods. Your server controls the identifier in method five. Explicit references are handled directly:

- A conversation ID or `previous_response_id` that does not identify an API conversation in this workspace returns `404 conversation_not_found` or `404 previous_response_not_found`, rather than creating another conversation.
- An assistant mismatch returns `400 conversation_model_mismatch`.
- Combining `previous_response_id` and `conversation` returns `400 conflicting_parameters`.

For the first two errors, discard the reference and start a new conversation.

The browser controls method four (`useChat` ID) and method six (fingerprint). They continue a conversation only if **all four** conditions hold:

1. **The assistant owns it.** Neither method can resume a team-owned conversation.
2. **History contains the latest recorded exchange verbatim:** the final visitor message and its entire reply, including staff messages added in the meantime.
3. **The assistant generated at least one message in that reply for this conversation.** Shared stock phrases, staff messages, product/service/file shares and the configured greeting are insufficient because another visitor could know them too.
4. **History starts at the first message** and includes every recorded visitor message in order. The latest exchange alone is insufficient because it may be predictable, such as “Thanks” followed by the same courtesy phrase. If only the last 40 messages were imported when creating the conversation, older messages may remain at the start of submitted history.

Comparison ignores whitespace and newlines, and merges consecutive messages with the same role. Visitor history imported through a publishable key does not break matching. Retrying the latest message with its own identity is a separate exception: [Retries](#tekrar-denemek).

If any condition fails, a new conversation starts. This includes:

- Team ownership.
- A latest reply containing only stock text, staff text or a share.
- Visitor messages not yet answered, such as silent or rejected turns. An empty assistant placeholder (`content: ""` or `null` in Chat Completions, or an AI SDK assistant without text) does not help.
- A staff reply missing from submitted history.
- A latest reply containing only the configured greeting.
- Truncated history that no longer starts at the first message, for example after sending only new text with an ID and later dropping that ID.
- An exact retry of an unanswered request without message identity.

These inputs can be guessed. Using them alone could attach another visitor and expose staff replies. A `useChat` ID or fingerprint shared by two conversations is discarded permanently. Ambiguity splits the conversation.

One remaining limitation: someone who reproduces an entire history verbatim may match a conversation through these two methods. In practice, this can happen after one predictable first exchange, such as “Hello” and an identical generated reply. A second visitor message closes that path. Publishable-key fingerprints are also bound to the visitor's network address, so only someone on the same network can match; changing networks opens a new conversation. **Send `conversation_id` on every request.**

Also give each new message an `Idempotency-Key`. Conversation IDs arrive in `X-Sone-Conversation-Id` and in the response body: Chat Completions `sone.conversation_id`, Responses `conversation.id`, or AI SDK assistant `metadata.conversationId`.

## Retries

After network errors, timeouts or `5xx` responses, retrying should not duplicate messages or replies. A retry must first find the conversation through its ID, a response reference (`previous_response_id` or `item_reference`), or the message's identity. History without an ID or identity cannot find an unanswered retry.

- **`Idempotency-Key`** names the newest visitor message, up to 200 characters. Same key and text stores it once; an existing reply is replayed, or an unanswered message is answered now. Different text with the same key returns `422 idempotency_key_reused` with `x-should-retry: false`. Generate a new key per new message.
- **Vercel AI SDK** names visitor messages using their `id` and text automatically. Editing text while retaining the same ID creates a new message.
- **Messages without identity** can be aligned against the history tail when the request has a conversation ID. Previously answered messages replay their replies; messages stored during rejected requests are not stored twice. Writing the same text again after receiving its reply is a new message. A retry containing still-unanswered messages also does not duplicate them; if answered in the meantime, their replies are replayed. However, if staff wrote after your message, an unnamed retry can duplicate it. Use an `Idempotency-Key` for each message. Without an ID or identity, history-only retries start a new conversation, including rejected or team-waiting messages resent with new messages. With Responses, the exchange referenced by `previous_response_id` or `item_reference` can substitute for history when sending only new input.
- **Neither history nor key:** a client sending only conversation ID and new text cannot distinguish a retry from a new message. Always use `Idempotency-Key` in this case.
- **Publishable keys without a conversation or response reference**, such as first messages, bind message identity to the network address and, for AI SDK, `useChat` ID. Switching from Wi-Fi to mobile data opens another conversation on retry. Two visitors sharing an address can have first messages merged if they submit the same `Idempotency-Key`, or the same AI SDK chat/message IDs, with identical text. Generate these randomly per visitor; AI SDK does so by default. Retries with a conversation or response reference work from any address.

Also:

- A rejected first message, such as `ai_turn_ceiling`, `plan_required` or `assistant_paused`, creates no conversation and stores nothing. It can be resent later.
- A still-running turn returns `409 conversation_busy`. Wait for `Retry-After` and resend with the same key.
- OpenAI SDKs do not add `Idempotency-Key`. Pass `headers` in Node's second request argument, or Python's `extra_headers`: [Chat Completions](https://docs.sonecentral.com/en/chat-completions.md#tekrar-denemek). SDK retries reuse that header.

With `fetch`:

```js title="gonder.js"
// Try the same message up to four times, preserving Idempotency-Key.
export async function sendOnce(url, key, body) {
  const name = crypto.randomUUID(); // identity for this message
  let response;
  for (let attempt = 0; attempt < 4; attempt += 1) {
    if (attempt > 0) await new Promise((done) => setTimeout(done, waitSeconds(response, attempt) * 1000));
    try {
      response = await fetch(url, {
        method: 'POST',
        headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', 'Idempotency-Key': name },
        body: JSON.stringify(body),
      });
    } catch {
      response = undefined; // network failure: retry
      continue;
    }
    const retry =
      response.headers.get('x-should-retry') !== 'false' &&
      (response.status === 409 || response.status === 429 || response.status >= 500);
    if (!retry) return response;
  }
  if (response) return response;
  throw new Error('Could not send the message.');
}

function waitSeconds(response, attempt) {
  const after = Number(response?.headers.get('retry-after'));
  return after > 0 && after <= 60 ? after : 2 ** (attempt - 1);
}
```

## Browser persistence

Store the conversation ID and displayed messages in `localStorage`. Refreshing or navigating to another page then resumes the chat.

```js title="sohbet-kaydi.js"
const STORE = 'sone-chat-v1';

export function loadChat() {
  try {
    return JSON.parse(localStorage.getItem(STORE)) ?? { conversationId: null, messages: [] };
  } catch {
    return { conversationId: null, messages: [] };
  }
}

export function saveChat(chat) {
  try {
    localStorage.setItem(STORE, JSON.stringify(chat));
  } catch {
    // Private browsing or full storage: keep this tab working; refresh starts over.
  }
}

export function forgetChat() {
  try {
    localStorage.removeItem(STORE);
  } catch {}
}
```

After every turn:

```js
const chat = loadChat();
chat.conversationId = sone.conversation_id; // from the response
chat.messages.push({ role: 'user', content: text }, ...sone.messages.map((m) => ({ role: 'assistant', message: m })));
saveChat(chat);
```

Send the ID on the next request:

```js
headers: {
  Authorization: `Bearer ${KEY}`,
  'Content-Type': 'application/json',
  'Idempotency-Key': crypto.randomUUID(), // reuse on retries
},
body: JSON.stringify({
  model: 'default',
  conversation_id: chat.conversationId ?? undefined,
  messages: [{ role: 'user', content: text }],
}),
```

With an explicit ID, sending only the new message is enough. An `Idempotency-Key` is required when doing so.

A “New chat” button calls `forgetChat()` so the next message starts fresh. Also clear the stored ID after `404 conversation_not_found` or `400 conversation_model_mismatch`. Complete example: [Embedded chat widget](https://docs.sonecentral.com/en/sohbet-penceresi.md).

## Vercel AI SDK

`useChat` retains the conversation ID in assistant messages' `metadata.conversationId`, which goes back with history. Save and restore both the `useChat` ID and messages, **including assistant metadata**. Saving only `id` is insufficient: browser-generated IDs require the [four conditions](#sohbet-nasil-bulunur) and never resume team-owned conversations.

```tsx
'use client';

import { useChat } from '@ai-sdk/react';
import type { ChatTransport } from 'ai';
import { useEffect, useState } from 'react';
import type { SoneUIMessage } from '@/lib/sone';

function storedChat(): { id: string; messages: SoneUIMessage[] } {
  try {
    const saved = JSON.parse(localStorage.getItem('sone-chat-v1') ?? 'null');
    if (saved?.id) return saved;
  } catch {}
  return { id: crypto.randomUUID(), messages: [] };
}

export function useStoredChat(transport: ChatTransport<SoneUIMessage>) {
  const [initial] = useState(storedChat);
  const chat = useChat<SoneUIMessage>({ id: initial.id, messages: initial.messages, transport });

  useEffect(() => {
    if (chat.status === 'submitted' || chat.status === 'streaming') return;
    try {
      localStorage.setItem('sone-chat-v1', JSON.stringify({ id: initial.id, messages: chat.messages }));
    } catch {}
  }, [chat.status, chat.messages, initial.id]);

  return chat;
}
```

Run this hook only in the browser. In Next.js, load its component from a `'use client'` file with `dynamic(() => import(...), { ssr: false })`. Sending just the latest message in long chats: [Vercel AI SDK](https://docs.sonecentral.com/en/vercel-ai-sdk.md#uzun-sohbetlerde-yalniz-son-mesaji-gondermek). Complete example: [Embedded chat widget](https://docs.sonecentral.com/en/sohbet-penceresi.md#react-ve-nextjs).

## Server keys and `user`

For users authenticated in your system, put your internal user ID in `user`. The same identifier and assistant find the same conversation across devices.

This is conversation lookup, not proof of identity. The assistant still treats the person as anonymous and does not disclose personal records: [Orders, appointments and privacy](https://docs.sonecentral.com/en/siparis-randevu-gizlilik.md). Publishable keys ignore `user` because visitors can alter it.

Use an opaque identifier, never an email address or phone number.

## Limits

- Up to 200 visitor messages per conversation; then `400 conversation_full`. Start a new chat.
- Up to 12 new visitor messages per request.
- Up to 4,000 characters per new message.
- Up to 40 previous messages imported into a new conversation.
- Request bodies up to 1 MB. Send the conversation ID instead of repeating long histories.
