-`chatbot.MaxTurnCharsCeiling = 65536` — upper bound for `CHATBOT_MAX_TURN_CHARS`, locked to the OpenAPI `BotTurnAddRequest.prompt.maxLength`. Validate() rejects any value above this.
| POST | `/client/{clientId}/bot/sessions` | `createBotSession` | Create a new owner-scoped session. |
| GET | `/client/{clientId}/bot/sessions` | `listBotSessions` | List the caller's sessions; bounds: `limit in [1,200]`, `offset >= 0`. |
| GET | `/client/{clientId}/bot/sessions/{sessionId}` | `getBotSession` | Read one owner-scoped session. |
| PATCH | `/client/{clientId}/bot/sessions/{sessionId}/scope` | `patchBotSessionScope` | Add/remove documents and folders from session scope. |
| POST | `/client/{clientId}/bot/sessions/{sessionId}/turns` | `createBotTurn` | Submit a new turn. Plan §6 algorithm. |
| GET | `/client/{clientId}/bot/sessions/{sessionId}/turns` | `listBotTurns` | List all turns for an owner-scoped session, ASC. |
| GET | `/client/{clientId}/documents/{documentId}/schema` | `getDocumentSchema` | Return the document's bound JSON Schema or `null` (omitted in JSON when unbound). |
| GET | `/client/{clientId}/documents/{documentId}/all-metadata` | `getDocumentAllMetadata` | Bundle of (document, labels, customMetadata) for one doc. |
| 404 | Cross-client / cross-user / soft-deleted session; document not found in client scope |
| 409 | `turn_in_flight`, `prior_turn_in_flight`, `already_complete`, `turn_session_deleted`, `ordinal_out_of_range`, `turn_superseded` (legacy: `prompt_mismatch` — service-layer assertion only; not reachable from the wire as of 2026-05-07) |
| `agent_application_error` | FastAPI returned 200 with `status:"error"`. Code/message in the inner `error` field. | Client may resubmit the same prompt at the same ordinal; the algorithm reset path runs. |
| `agent_missing_answer` | FastAPI returned 200 with empty/null/whitespace `answer.text`. | Same as above. May indicate a prompt the agent cannot answer; client UX should suggest rephrasing. |
| `agent_invalid_response` | FastAPI returned 200 but body failed structural validation (malformed JSON or oversize completion). | Same as above. May indicate an upstream agent change; check upstream logs. |
| `agent_transport_error` | FastAPI exhausted its 3-attempt retry budget on transport-level failures (429/5xx/timeouts). | Same as above. May indicate FastAPI overload; check upstream health and rate-limit. |
To retry: client POSTs to the same `/turns` route with the same prompt.
The handler picks the `errored` row's ordinal via prompt-match;
`AddTurn` runs the existing-row reset path; a new `attempt_id` is
minted; tx2 records the new outcome.
If the client posts a *different* prompt instead of retrying, the
handler advances to `last_terminal_ordinal+1` (a fresh `in_flight` at
ordinal N+1) rather than rejecting with 409 `prompt_mismatch`. The
errored row at ordinal N stays in history. See
`plans/chatbot.stuff/fix.chat.bugs.1.md` §3 Option A for the rationale.
#### `abandoned`
The grace window expired without a terminal status. Most often this
means the queryAPI process died mid-call and the next caller's
`AbandonExpiredInflightForOwner` swept the stale row. The error JSON
carries:
```json
{"code":"turn_abandoned_grace_expired","message":"no terminal status reached within grace window"}
```
Recovery is identical to `errored`: client resubmits the same prompt.
The reset path applies.
#### `session_deleted`
A super-admin DELETEd the session while the turn was in flight, OR an
older retry path hit the session-deleted-during-call branch. The error
JSON carries:
```json
{"code":"session_deleted_by_admin","message":"session deleted while turn in flight","attempt":<int>}
```
OR (from the AddTurn tx2 path):
```json
{"code":"session_deleted_during_call","message":"session was soft-deleted while FastAPI call was in flight","attempt":<int>}
```
Recovery: NONE. The session is permanent. The client must create a new
session and resubmit. The historical `session_deleted` row remains for
audit; LIST turns surfaces it with the original prompt and the error
JSON.
### Performance notes
`decorateSessionList` (the M2 list-sessions handler path) does N+1
scope reads: one `loadScope` round-trip per session in the list.
Default `limit=20`, hard cap `limit=200`. Under default load the
overhead is negligible; if a deployment surfaces latency on the list
endpoint, consider a single batch read of all scope rows for the
listed session ids. Plan v10 marked this as acceptable for v1.
### Rate-limit overrides
`RATE_LIMIT_ENDPOINT_OVERRIDES` is a JSON object keyed by
`METHOD /path` using **Echo's path template form** (`:clientId`,
`:sessionId`), NOT the OpenAPI `{clientId}` form. The middleware uses
`c.Path()` which returns Echo's registered path string; an override
keyed by `{clientId}` will silently fail to match.