OpenClaw Agent — Data Model
База: agent_db (PostgreSQL, database-per-service).
ER-диаграмма
erDiagram agent_conversations ||--o{ agent_messages : has agent_conversations { uuid id PK uuid user_id "владелец сессии" uuid franchise_id "tenant" text title "первые 60 символов первого user-message" text status "active | closed" timestamptz created_at timestamptz updated_at } agent_messages { uuid id PK uuid conversation_id FK text role "user | assistant | tool" text content "текст; для tool — JSON-результат" jsonb tool_calls "массив, только для role=assistant" text tool_call_id "только для role=tool" int4 llm_duration_ms "только для role=assistant" int4 eval_tokens "только для role=assistant" timestamptz created_at }
Таблица agent_conversations
| Поле | Тип | Constraints | Описание |
|---|---|---|---|
id | UUID | PK | |
user_id | UUID | NOT NULL | Владелец. Источник — JWT |
franchise_id | UUID | NOT NULL | Для мультитенантной изоляции |
title | TEXT | NOT NULL | Первые 60 символов первого user-message, auto |
status | TEXT | NOT NULL, default active, CHECK in (active, closed) | |
created_at | TIMESTAMPTZ | NOT NULL, default now() | |
updated_at | TIMESTAMPTZ | NOT NULL, default now() | Обновляется на каждом INSERT в agent_messages через trigger |
Индексы:
idx_conversations_user_updatedON (user_id,updated_at DESC) — для списка сессий пользователяidx_conversations_franchiseON (franchise_id) — для админ-аудита
Бизнес-правила:
- Удаление сессии каскадно удаляет все её
agent_messages - При создании первого user-message сразу же создаётся
conversation(один тип запроса в БД —INSERT … ON CONFLICT) closedсессии не принимают новые сообщения (409 CONVERSATION_LIMIT_REACHED)
Таблица agent_messages
| Поле | Тип | Constraints | Описание |
|---|---|---|---|
id | UUID | PK | |
conversation_id | UUID | NOT NULL, FK → agent_conversations(id) ON DELETE CASCADE | |
role | TEXT | NOT NULL, CHECK in (user, assistant, tool, system) | |
content | TEXT | NULL | Для assistant с tool_calls — может быть NULL. Для tool — JSON-string результата |
tool_calls | JSONB | NULL | Массив [{id, name, arguments_json}], только для role=assistant |
tool_call_id | TEXT | NULL | Только для role=tool — ID вызова, на который отвечаем |
llm_duration_ms | INT4 | NULL | Длительность inference (только assistant) |
eval_tokens | INT4 | NULL | Кол-во сгенерированных токенов (только assistant) |
created_at | TIMESTAMPTZ | NOT NULL, default now() |
Индексы:
idx_messages_conversation_createdON (conversation_id,created_at ASC) — для чтения историиidx_messages_tool_call_idON (tool_call_id) WHERErole='tool'— для матчинга tool_call → tool_result
Бизнес-правила:
role='user'→ content NOT NULL, tool_calls NULL, tool_call_id NULLrole='assistant'→ tool_calls может быть NOT NULL (если модель вызвала tool) ИЛИ content NOT NULL (финальный ответ). Может быть и то, и то.role='tool'→ content NOT NULL (JSON-результат), tool_call_id NOT NULL, tool_calls NULLrole='system'→ внутреннее, не пишется в БД в MVP (системный промпт хардкодом в коде)
Маскирование при чтении:
- При выдаче через
GET /conversations/{id}content дляrole='tool'отдаётся как есть (это уже маскированный результат — маскирование происходит в OpenClaw Agent перед записью в БД) - PII-фильтр в Agent дополнительно маскирует
password,pin_hashесли приходят вcontent— двойная защита
Trigger: обновление updated_at в conversations
CREATE OR REPLACE FUNCTION update_conversation_timestamp()
RETURNS TRIGGER AS $$
BEGIN
UPDATE agent_conversations
SET updated_at = NEW.created_at
WHERE id = NEW.conversation_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_messages_update_conversation
AFTER INSERT ON agent_messages
FOR EACH ROW EXECUTE FUNCTION update_conversation_timestamp();Cron-job: auto-cleanup
Раз в сутки (Liquibase + pg_cron / встроенный node-cron):
DELETE FROM agent_conversations
WHERE status = 'closed' AND updated_at < now() - interval '30 days';Active сессии не трогаем (юзер может вернуться).
Redis-ключи (вспомогательные)
| Ключ | Тип | TTL | Назначение |
|---|---|---|---|
agent:rate:user:{user_id}:{minute} | INT | 70 сек | Rate-limit per user (30/min) |
agent:stream:{conversation_id} | STRING (request_id) | 5 мин | Lock, чтобы юзер не открыл два параллельных стрима на одну сессию |
Связь с другими сервисами
| Сервис | Что хранится у них | Что у нас |
|---|---|---|
| User Service | users, permissions_roles | user_id (denormalized в conversations) |
| Catalog Service | products, stop_list | результаты tool в agent_messages.content |
| Order Service | orders | результаты tool, order_id в content (для трассировки) |