CONVERSATIONS_* setting with its default.
Every routing operation requires the Redis conversations backend
(CONVERSATIONS_REDIS_URL). With no backend, each one refuses with a loud 501.
Routes: CRUD and CLI
create is an authority-changing, admin-adjacent operation: it binds the
route’s execution_key (a pass-role decision) before any write, so a refused bind
leaves an existing row untouched. agent_name must merely exist. A channel row’s
(channel, our_identity) pair must be unclaimed. An api row mints a
callback_secret returned once in the create result and never re-readable.
create flags: --door (api|channel), --agent, --execution-key,
--channel / --identity (channel rows), --callback-url (api rows, HTTPS).
The message door
POST /api/conversations/{route_name}/messages (authed) accepts one API-door
message and runs its turn as the route’s execution key. Body: external_user_id,
text, optional wait_seconds.
202 {message_id, thread_id}— accepted, answer delivered later by signed callback (default).200with the answer inline — await_secondsturn that finished in time; its callback is suppressed so it never double-fires.wait_secondsis clamped tosync_wait_max_seconds.- Refusals are loud:
400bad body,401unauthorized,404unknown route,429address rate cap,501no backend,503thread queue full.
The read doors
The single-record door is caller-scoped: an API-door record is readable by the
caller that invoked the turn or by an admin; a channel-door record is admin-only. A
non-owning caller gets
403; a record on another route (or missing) is 404. An
admin read returns the full record (including error, the turn’s internal detail);
the caller read returns a caller-safe projection that withholds error and the
delivery bookkeeping — the turn ran as the route’s key, not the caller’s.
The failed-delivery listing spans every route and caller, so it is admin-only.
The delivery lifecycle
An answer record moves throughaccepted → pending_delivery → provisional →
delivered, or ends failed (undelivered past the attempt budget, or reported
failed by the provider) or shed (refused by the address rate cap; no turn ran).
Only the terminal states (delivered, failed, shed) carry the retention TTL.
Delivery is retried with exponential backoff; a provisional record awaits an
out-of-band receipt until delivery_grace_seconds elapses. A restart re-drives
every unfinished record, delivering exactly once (no double callback).
Client-facing text on the failure paths is fixed, not configurable:
- Turn error —
"Sorry, something went wrong handling your message. Please try again."Anerroranswer never carries an internal detail; the detail is retained in the record’serrorfield for the admin read door only. - Rate-cap slow-down —
"You are sending messages faster than I can answer. Please wait a moment and try again."— sent once per cooldown window; further over-limit messages are dropped asshed.
Checkpoint retention
Conversation continuity is the checkpoint store’s. Idle-thread expiry is governed by the LLM checkpoint settings (theLLM_PROVIDER_* group), not a CONVERSATIONS_*
setting:
checkpoint_ttl_minutes is an idle TTL: a thread lives as long as it keeps
receiving messages and expires only after it sits idle past the lifetime. Unset
means threads are kept forever.
How the idle window is enforced depends on the provider:
rediscarries a native per-key TTL, refreshed on each checkpoint write — idle expiry is automatic.postgres/sqlitehave no native expiry; the sweep operation deletes every thread whose newest checkpoint is older than the cutoff.memoryis process-lifetime.
The sweep operation
sweep_checkpoints (POST /api/checkpoints/sweep, tai checkpoints sweep) is
admin-tier (a fenced, deployment-wide destructive memory purge). It deletes
stale threads on a DB-backed provider and is a reported no-op on redis/memory
or with the TTL unset. It runs on either recurrence path:
- Native schedules — recurrence needs the tool branched with the backend’s
schedule_taskextension: wrap the sweep in a small manifest tool carryingschedule_task, then schedule that branched tool viaPOST /api/schedules(tai schedules …) on a scheduler-capable backend. (Projecting as a tool makes the sweep runnable by name; theschedule_taskbranch is what registers a recurrence.) - External cron — call
tai checkpoints sweepon a timer from your own cron / systemd timer / k8s CronJob. The busless path — no scheduler backend needed.
The fire-path authorization model
A conversation route separates who may send from what the agent may do, and both are enforced live. Who may send. Achannel message is authenticated by the provider’s own
signature over the raw webhook body (fail-closed — a missing or bad signature is
rejected and runs no turn). An api message is authenticated by the access-control
gate on the send door. This mirrors the general
trigger authentication posture — a door is
public, verifier-signed, token-gated, or API-key-gated — applied to the two
conversation doors.
What the agent may do. The turn runs as the route’s bound execution_key,
never as the sender. That key’s live stored grants authorize the agent run and
every tool call the turn makes — including any tool a deep-agent sub-agent calls.
The rules:
- Bind (pass-role). You may bind your own identity or an execution key you own;
an admin may bind any key. A non-admin’s refusal is a uniform
403across absent-key and not-yours, so the door is no probe for key ids. - Token-free-evaluable only. The bound key’s stored policy condition must be resolvable by a background execution with no request token. A condition that needs a request token is rejected at bind — it could never be evaluated at fire.
- Fingerprint binding. The route stores a server-derived per-mint
execution_key_fingerprint. The fire resolves the identity against that fingerprint, so a key re-minted under a new fingerprint no longer satisfies the route until it is re-bound. - Enforce at fire, automatic revocation. The turn is authorized against the key’s grants at fire time, not at bind. Attenuate the key by any means — drop a scope, disable it, delete it, narrow its owner — and the next turn is denied, with no revocation-specific step on the route. Scope the key (and the agent) to least privilege.
error outcome, its
detail retained for the admin read door.
Signed-callback posture. An api route’s callback_url must be absolute
HTTPS — an http:// or relative URL is rejected at create, with no insecure
opt-out. Every callback is signed: X-Tai-Signature: sha256=<HMAC-SHA256(callback_secret, raw_body)>. See connect your own system.
Settings
Every setting reads theCONVERSATIONS_ env prefix (so max_concurrent_turns is
CONVERSATIONS_MAX_CONCURRENT_TURNS). Startup validators reject an invalid
combination loudly.
Backend
Turn-engine bounds
Delivery bounds
Retention / idempotency
See also
- Client conversations — the model.
- WhatsApp, Telegram / Slack, and your own system — the setup guides.
- Access control and owned keys — the execution key’s grants.

